// ==UserScript==
// @name 币圈KOL劣迹标记 - X(Twitter)
// @namespace http://tampermonkey.net/
// @icon data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAB7ElEQVR4Ae1XMZLCMAwUdw0ldJQ8ATpKnkBJByUd8ALyA/gBdJTQUtHS8QT4AaRM5ctmThmfogQ75CYNmhGTbGJr45Vk0yAiQzXaF9VsHwIZAofDgYwxqo9GI/K16/X6cqyxvdVqmdvtZh6PhwmCIHXcw7vdrpFj8ny9XhsYxhe8lwWHw2EycLFYpNh0Ok2w8/nsFHy1WrkE1wnAN5tNMkGv10ux3W6XIab5fD5P3ovldCGrP2Ap4LiW8uRJAcIwe1wpArYU0FJimhQgxaQ9cqX4BZYCgSVmS8HBfRP1JQEsY1xKGSmAcTC+l0QrIWDraicVMBBA4O1265ScpQnAMbkMwphjub1HAI7EkxoDK7n0/gQQGATsCmDMo+z++Hf8E5CjPZ9PiqKIZrMZhWFIl8slxcbjMTWbTTqdTuRrXoz5i2WXRIL+WxWw2+Uml13rnJUT4K9E9nMFaF3SxiojoO1u2rJzl4z3/+oIcHBMLiUp2rDe3ozg+BIYtNee87KjGzLGndPx7JD/0K7xog2Gl30ymaSY1jm9CPhsrXnnBK1zOhHgCWWtF7l2TtA6p3S1E+73exoMBrRcLul4PJKL3e93arfbSUeMA1O/36eYPHU6nWQu7pyaqRlfZnezV05anhSN34va7PPXrHYCP+VaTG3LBV1KAAAAAElFTkSuQmCC
// @version 1.2
// @description 在Twitter/X上标记有劣迹的加密货币KOL,并实时显示其劣迹指数(0-100分)和不良行为记录。整合公共数据和本地自定义功能,帮助用户识别高风险KOL,避免投资陷阱。支持证据查看、数据更新和自定义标记功能。
// @author @mr96_0x0 (TG: @Mr96_me)
// @license GNU General Public License v3.0 or later
// @match https://twitter.com/*
// @match https://x.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @connect *
// ==/UserScript==
(function() {
'use strict';
const developerInfo = {
twitter: "@mr96_0x0",
telegram: "@Mr96_me"
};
const PUBLIC_DATA_URL = "https://gist.githubusercontent.com/Mr96s/b05aa6971cea6407bcb00621b6c20197/raw/ade9ff6ba76472aca8b65e950ff626f2cc7baad9/kol-data.json";
const indexColors = {
0: { bg: "#e0e0e0", text: "#000000", emoji: "✅" }, // 0-9
10: { bg: "#ccffcc", text: "#000000", emoji: "🔍" }, // 10-19
20: { bg: "#99ff99", text: "#000000", emoji: "🔍" }, // 20-29
30: { bg: "#ffff99", text: "#000000", emoji: "⚠️" }, // 30-39
40: { bg: "#ffeb3b", text: "#000000", emoji: "⚠️" }, // 40-49
50: { bg: "#ffcc00", text: "#000000", emoji: "⚠️" }, // 50-59
60: { bg: "#ff9800", text: "#ffffff", emoji: "🚨" }, // 60-69
70: { bg: "#ff5722", text: "#ffffff", emoji: "🚨" }, // 70-79
80: { bg: "#f44336", text: "#ffffff", emoji: "☠️" }, // 80-89
90: { bg: "#d32f2f", text: "#ffffff", emoji: "☠️" } // 90-100
};
function getColorConfig(index) {
const tier = Math.min(Math.floor(index / 10) * 10, 90);
return indexColors[tier] || indexColors[50];
}
let badKOLs = {};
let usePublicData = GM_getValue('usePublicData', true);
let useLocalData = GM_getValue('useLocalData', true);
function loadData() {
const publicData = GM_getValue('publicData', {});
const localData = GM_getValue('localData', {});
badKOLs = {};
if (usePublicData) Object.assign(badKOLs, publicData);
if (useLocalData) Object.assign(badKOLs, localData);
checkForKOLs();
}
function fetchPublicData() {
if (!usePublicData) return;
GM_xmlhttpRequest({
method: "GET",
url: PUBLIC_DATA_URL,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
GM_setValue('publicData', data);
loadData();
} catch (e) {
console.error("Failed to parse public data:", e);
}
},
onerror: function(error) {
console.error("Failed to fetch public data:", error);
}
});
}
const observer = new MutationObserver(checkForKOLs);
observer.observe(document.body, { childList: true, subtree: true });
function checkForKOLs() {
const profileHeader = document.querySelector('[data-testid="UserName"]');
if (profileHeader) {
const screenName = document.querySelector('[data-testid="UserName"] div:nth-child(2) div span')?.textContent;
if (screenName && badKOLs[screenName]) {
addWarningBadge(profileHeader, badKOLs[screenName], true, screenName);
}
}
document.querySelectorAll('[data-testid="tweet"]').forEach(tweet => {
const authorLink = tweet.querySelector('a[role="link"][tabindex="-1"]');
if (authorLink) {
const author = authorLink.getAttribute('href')?.slice(1);
if (author && badKOLs[author]) {
addWarningBadge(tweet, badKOLs[author], false, author);
}
}
});
}
function addWarningBadge(element, kolData, isProfile, accountName) {
if (element.querySelector('.kol-warning-badge')) return;
const colorConfig = getColorConfig(kolData.index);
const badgeSize = isProfile ? '16px' : '12px';
const badge = document.createElement('div');
badge.className = 'kol-warning-badge';
badge.innerHTML = `
<div style="
background: ${colorConfig.bg};
color: ${colorConfig.text};
padding: 4px 8px;
border-radius: 4px;
font-weight: bold;
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: 8px;
cursor: pointer;
font-size: ${badgeSize};
line-height: 1;
white-space: nowrap;
">
${colorConfig.emoji}劣迹指数:${kolData.index}
</div>
`;
badge.onclick = (e) => {
e.stopPropagation();
showKolDetails(kolData, accountName, element);
};
if (isProfile) {
const nameElement = element.querySelector('div:nth-child(2) div span');
if (nameElement) nameElement.parentNode.appendChild(badge);
} else {
const authorContainer = element.querySelector('[data-testid="User-Name"]');
if (authorContainer) authorContainer.appendChild(badge);
}
}
function showKolDetails(kolData, accountName, badgeElement) {
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(0,0,0,0.7); display: flex;
justify-content: center; align-items: center; z-index: 9999;
`;
const content = document.createElement('div');
content.style.cssText = `
background-color: #15202b; color: #ffffff; padding: 20px;
border-radius: 12px; max-width: 500px; width: 90%;
max-height: 80vh; overflow-y: auto;
`;
const colorConfig = getColorConfig(kolData.index);
let displayName = null;
const profileNameElement = document.querySelector('[data-testid="UserName"] div:first-child span');
if (badgeElement.closest('[data-testid="UserName"]')) {
displayName = profileNameElement ? profileNameElement.textContent : null;
} else {
const tweetElement = badgeElement.closest('[data-testid="tweet"]');
if (tweetElement) {
const tweetNameElement = tweetElement.querySelector('[data-testid="User-Name"] a div span');
displayName = tweetNameElement ? tweetNameElement.textContent : null;
} else {
displayName = profileNameElement ? profileNameElement.textContent : null;
}
}
const finalName = displayName || `@${accountName}`;
const title = document.createElement('h2');
title.textContent = `⚠️ 【${finalName}】 的 劣迹指数 (${kolData.index})`;
title.style.cssText = `margin-top: 0; color: ${colorConfig.bg};`;
content.appendChild(title);
const recordsTitle = document.createElement('h3');
recordsTitle.textContent = '劣迹记录:';
recordsTitle.style.marginBottom = '8px';
content.appendChild(recordsTitle);
const recordsList = document.createElement('ul');
recordsList.style.cssText = 'padding-left: 20px; margin-top: 0;';
kolData.records.forEach(record => {
const recordItem = document.createElement('li');
recordItem.style.marginBottom = '12px';
const reason = document.createElement('div');
reason.textContent = record.reason;
reason.style.marginBottom = '4px';
recordItem.appendChild(reason);
if (record.date && record.date.trim()) {
const date = document.createElement('div');
date.textContent = `时间: ${record.date}`;
date.style.cssText = 'font-size: 0.9em; color: #8899a6; margin-bottom: 4px;';
recordItem.appendChild(date);
}
if (record.proof && record.proof.trim()) {
const proofLink = document.createElement('a');
proofLink.href = record.proof;
proofLink.textContent = '查看详情 →';
proofLink.target = '_blank';
proofLink.style.cssText = 'color: #1da1f2; text-decoration: none; font-size: 0.9em;';
recordItem.appendChild(proofLink);
}
recordsList.appendChild(recordItem);
});
content.appendChild(recordsList);
const feedbackSection = document.createElement('div');
feedbackSection.style.cssText = 'margin-top: 20px; padding-top: 15px; border-top: 1px solid #38444d;';
feedbackSection.innerHTML = `
<h4 style="margin-bottom: 8px;">📢 反馈与申诉</h4>
<p style="margin-bottom: 8px; font-size: 0.9em;">1.🌟欢迎提交线索!共建币圈透明社区</p>
<p style="margin-bottom: 8px; font-size: 0.9em;">2.🗃️数据来源由程序爬取公开信息或用户提交,如您认为标记信息有误,可提交申诉。</p>
<h4 style="margin-bottom: 8px;">📬 请通过以下方式提交线索或申诉:</h4>
<a href="https://twitter.com/${developerInfo.twitter}" target="_blank" style="display: block; color: #1da1f2; text-decoration: none; font-size: 0.9em; margin-bottom: 4px;">🐦 Twitter: ${developerInfo.twitter}</a>
<a href="https://t.me/${developerInfo.telegram.replace('@', '')}" target="_blank" style="display: block; color: #1da1f2; text-decoration: none; font-size: 0.9em;">📨 Telegram: ${developerInfo.telegram}</a>
`;
content.appendChild(feedbackSection);
const closeButton = document.createElement('button');
closeButton.textContent = '关闭';
closeButton.style.cssText = 'margin-top: 15px; padding: 8px 16px; background-color: #1da1f2; color: white; border: none; border-radius: 4px; cursor: pointer;';
closeButton.onclick = () => document.body.removeChild(modal);
content.appendChild(closeButton);
modal.appendChild(content);
document.body.appendChild(modal);
modal.onclick = (e) => {
if (e.target === modal) document.body.removeChild(modal);
};
}
function showLocalDataEditor() {
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(0,0,0,0.7); display: flex;
justify-content: center; align-items: center; z-index: 9999;
`;
const content = document.createElement('div');
content.style.cssText = `
background-color: #15202b; color: #ffffff; padding: 20px;
border-radius: 12px; max-width: 600px; width: 90%;
max-height: 80vh; overflow-y: auto;
`;
const title = document.createElement('h2');
title.textContent = '✏️ 编辑本地KOL记录';
title.style.cssText = 'margin-top: 0; color: #1da1f2;';
content.appendChild(title);
const localData = GM_getValue('localData', {});
const form = document.createElement('div');
form.innerHTML = `
<div style="margin-bottom: 12px;">
<label style="display: block; margin-bottom: 4px;">KOL用户名 (不含@):</label>
<input id="username" type="text" style="width: 100%; padding: 6px; background-color: #253341; color: #ffffff; border: 1px solid #38444d; border-radius: 4px;">
</div>
<div style="margin-bottom: 12px;">
<label style="display: block; margin-bottom: 4px;">劣迹指数 (0-100):</label>
<input id="index" type="number" min="0" max="100" value="50" style="width: 100%; padding: 6px; background-color: #253341; color: #ffffff; border: 1px solid #38444d; border-radius: 4px;">
</div>
<div style="margin-bottom: 12px;">
<label style="display: block; margin-bottom: 4px;">劣迹原因:</label>
<input id="reason" type="text" style="width: 100%; padding: 6px; background-color: #253341; color: #ffffff; border: 1px solid #38444d; border-radius: 4px;">
</div>
<div style="margin-bottom: 12px;">
<label style="display: block; margin-bottom: 4px;">证据链接 (可选):</label>
<input id="proof" type="text" style="width: 100%; padding: 6px; background-color: #253341; color: #ffffff; border: 1px solid #38444d; border-radius: 4px;" placeholder="https://...">
</div>
<div style="margin-bottom: 12px;">
<label style="display: block; margin-bottom: 4px;">日期 (可选):</label>
<input id="date" type="date" style="width: 100%; padding: 6px; background-color: #253341; color: #ffffff; border: 1px solid #38444d; border-radius: 4px;" value="${new Date().toISOString().split('T')[0]}">
</div>
<button id="addRecord" style="padding: 6px 12px; background-color: #1da1f2; color: white; border: none; border-radius: 4px; cursor: pointer;">添加记录</button>
`;
content.appendChild(form);
const usernameInput = form.querySelector('#username');
const indexInput = form.querySelector('#index');
const reasonInput = form.querySelector('#reason');
const proofInput = form.querySelector('#proof');
const dateInput = form.querySelector('#date');
const addButton = form.querySelector('#addRecord');
addButton.onclick = () => {
const username = usernameInput.value.trim();
const index = parseInt(indexInput.value);
const reason = reasonInput.value.trim();
if (!username || !reason) {
alert('用户名和劣迹原因不能为空');
return;
}
if (isNaN(index) || index < 0 || index > 100) {
alert('劣迹指数必须在 0-100 之间');
return;
}
if (!localData[username]) {
localData[username] = { index: index, records: [] };
} else {
localData[username].index = index;
}
localData[username].records.push({
reason: reason,
proof: proofInput.value.trim() || '',
date: dateInput.value.trim() || ''
});
GM_setValue('localData', localData);
loadData();
alert(`已添加 ${username} 的记录`);
usernameInput.value = '';
reasonInput.value = '';
proofInput.value = '';
updateRecordsList(recordsList, localData);
};
const recordsSection = document.createElement('div');
recordsSection.style.marginTop = '20px';
recordsSection.innerHTML = '<h3 style="margin-bottom: 8px;">已有本地记录</h3>';
const recordsList = document.createElement('ul');
recordsList.style.cssText = 'padding-left: 20px; margin-top: 0;';
updateRecordsList(recordsList, localData);
recordsSection.appendChild(recordsList);
content.appendChild(recordsSection);
const closeButton = document.createElement('button');
closeButton.textContent = '关闭';
closeButton.style.cssText = 'margin-top: 15px; padding: 8px 16px; background-color: #1da1f2; color: white; border: none; border-radius: 4px; cursor: pointer;';
closeButton.onclick = () => document.body.removeChild(modal);
content.appendChild(closeButton);
modal.appendChild(content);
document.body.appendChild(modal);
modal.onclick = (e) => {
if (e.target === modal) document.body.removeChild(modal);
};
}
function updateRecordsList(recordsList, localData) {
recordsList.innerHTML = '';
for (const [username, data] of Object.entries(localData)) {
data.records.forEach((record, index) => {
const li = document.createElement('li');
li.style.cssText = 'margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center;';
let recordText = `${username} (${data.index}): ${record.reason}`;
if (record.date && record.date.trim()) recordText += ` - ${record.date}`;
li.innerHTML = `
<span>${recordText}</span>
<button style="padding: 4px 8px; background-color: #ff3b30; color: white; border: none; border-radius: 4px; cursor: pointer;">删除</button>
`;
li.querySelector('button').onclick = () => {
data.records.splice(index, 1);
if (data.records.length === 0) delete localData[username];
GM_setValue('localData', localData);
loadData();
updateRecordsList(recordsList, localData);
};
recordsList.appendChild(li);
});
}
}
// 设置界面
function showSettings() {
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(0,0,0,0.7); display: flex;
justify-content: center; align-items: center; z-index: 9999;
`;
const content = document.createElement('div');
content.style.cssText = `
background-color: #15202b; color: #ffffff; padding: 20px;
border-radius: 12px; max-width: 600px; width: 90%;
max-height: 80vh; overflow-y: auto;
`;
const title = document.createElement('h2');
title.textContent = '⚙️ 设置';
title.style.cssText = 'margin-top: 0; color: #1da1f2;';
content.appendChild(title);
const dataSourceSection = document.createElement('div');
dataSourceSection.innerHTML = `
<h3 style="margin-bottom: 8px;">数据源设置</h3>
<label style="display: block; margin-bottom: 8px;">
<input type="checkbox" id="usePublicData" ${usePublicData ? 'checked' : ''}> 使用公共数据
</label>
<label style="display: block; margin-bottom: 8px;">
<input type="checkbox" id="useLocalData" ${useLocalData ? 'checked' : ''}> 使用本地数据
</label>
<button id="saveDataSource" style="padding: 6px 12px; background-color: #1da1f2; color: white; border: none; border-radius: 4px; cursor: pointer;">保存</button>
`;
content.appendChild(dataSourceSection);
const publicCheckbox = dataSourceSection.querySelector('#usePublicData');
const localCheckbox = dataSourceSection.querySelector('#useLocalData');
const saveButton = dataSourceSection.querySelector('#saveDataSource');
saveButton.onclick = () => {
usePublicData = publicCheckbox.checked;
useLocalData = localCheckbox.checked;
GM_setValue('usePublicData', usePublicData);
GM_setValue('useLocalData', useLocalData);
loadData();
alert('数据源设置已保存');
};
const localDataSection = document.createElement('div');
localDataSection.style.marginTop = '20px';
localDataSection.innerHTML = `
<h3 style="margin-bottom: 8px;">本地数据管理</h3>
<button id="editLocalData" style="padding: 6px 12px; background-color: #1da1f2; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 8px;">编辑本地数据</button>
<button id="clearLocalData" style="padding: 6px 12px; background-color: #ff3b30; color: white; border: none; border-radius: 4px; cursor: pointer;">清空本地数据</button>
`;
localDataSection.querySelector('#editLocalData').onclick = showLocalDataEditor;
localDataSection.querySelector('#clearLocalData').onclick = () => {
if (confirm('确定清空本地数据吗?')) {
GM_setValue('localData', {});
loadData();
alert('本地数据已清空');
}
};
content.appendChild(localDataSection);
const devInfoSection = document.createElement('div');
devInfoSection.style.cssText = 'margin-top: 20px; padding-top: 15px; border-top: 1px solid #38444d;';
devInfoSection.innerHTML = `
<h3 style="margin-bottom: 8px;">开发者信息</h3>
<p style="margin-bottom: 8px; font-size: 0.9em;">Twitter: <a href="https://twitter.com/${developerInfo.twitter}" target="_blank" style="color: #1da1f2; text-decoration: none;">${developerInfo.twitter}</a></p>
<p style="margin-bottom: 8px; font-size: 0.9em;">Telegram: <a href="https://t.me/${developerInfo.telegram.replace('@', '')}" target="_blank" style="color: #1da1f2; text-decoration: none;">${developerInfo.telegram}</a></p>
`;
content.appendChild(devInfoSection);
const closeButton = document.createElement('button');
closeButton.textContent = '关闭';
closeButton.style.cssText = 'margin-top: 15px; padding: 8px 16px; background-color: #1da1f2; color: white; border: none; border-radius: 4px; cursor: pointer;';
closeButton.onclick = () => document.body.removeChild(modal);
content.appendChild(closeButton);
modal.appendChild(content);
document.body.appendChild(modal);
modal.onclick = (e) => {
if (e.target === modal) document.body.removeChild(modal);
};
}
// 初始化菜单命令
GM_registerMenuCommand('⚙️ 打开设置', showSettings);
GM_registerMenuCommand('🔄 更新公共数据', () => {
fetchPublicData();
alert('正在更新公共数据...');
});
// 初始化
loadData();
fetchPublicData();
setInterval(fetchPublicData, 24 * 60 * 60 * 1000); // 每24小时更新一次公共数据
})();