币圈KOL劣迹标记 - X(Twitter)

在Twitter/X上标记有劣迹的加密货币KOL,并实时显示其劣迹指数(0-100分)和不良行为记录。整合公共数据和本地自定义功能,帮助用户识别高风险KOL,避免投资陷阱。支持证据查看、数据更新和自定义标记功能。

// ==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小时更新一次公共数据
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址