Greasy Fork is available in English.

X.com Chain Blocker

Block author, retweeters, repliers, and auto-block users based on rules. Manage block log, whitelist, and settings in a panel.

נכון ליום 30-06-2025. ראה הגרסה האחרונה.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         X.com Chain Blocker
// @name:zh-CN   X.com 九族拉黑
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  Block author, retweeters, repliers, and auto-block users based on rules. Manage block log, whitelist, and settings in a panel.
// @description:zh-CN 当拉黑作者时,自动拉黑所有转推者和回复者。支持自动拉黑(可根据长度、中文和斜杠数量等规则)、黑名单、白名单管理及详细配置。
// @author       Gemini 2.5 Pro
// @license      MIT
// @match        *://x.com/*
// @match        *://twitter.com/*
// @exclude      *://x.com/settings*
// @exclude      *://twitter.com/settings*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @connect      api.x.com
// @connect      x.com
// ==/UserScript==
(function () {
'use strict';
// --- CONFIG & CONSTANTS ---
const MENU_ITEM_TEXT = "九族拉黑 (包括回复)";
const STORAGE_KEY = 'CHAIN_BLOCKER_DATA';
const CONFIG_STORAGE_KEY = 'CHAIN_BLOCKER_CONFIG';
const BLOCK_INTERVAL_MS = 10 * 1000;
const PROCESS_CHECK_INTERVAL_MS = 5 * 1000;
const USERNAME_LENGTH_THRESHOLD = 25;
const AUTO_SCAN_INTERVAL_MS = 2000;
const API_RETRY_DELAY_MS = 5 * 60 * 1000;
let currentUserId = null, currentUserScreenName = null, activeTweetArticle = null;
let isProcessingQueue = false, processIntervalId = null, apiLimitCountdownInterval = null;
let scriptConfig = {}, isConfigPanelBusy = false;

// --- STYLES ---
GM_addStyle(`.nuke-toast{position:fixed;top:20px;right:20px;z-index:100000;background-color:#15202b;color:white;padding:10px 15px;border-radius:12px;border:1px solid #38444d;box-shadow:0 4px 12px rgba(0,0,0,0.4);width:auto;max-width:350px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;transition:all .5s ease-out;opacity:1;transform:translateX(0)}.nuke-toast.fading-out{opacity:0;transform:translateX(20px)}.nuke-toast-title{font-weight:bold;margin-bottom:8px;font-size:16px}.nuke-toast-status{font-size:14px;margin-bottom:0;line-height:1.5}#nuke-status-toast{background-color:#253341}#nuke-api-limit-toast{background-color:#d9a100;color:#15202b;border-color:#ffc107}.nuke-config-panel{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:100001;background-color:#15202b;color:white;border-radius:16px;border:1px solid #38444d;box-shadow:0 8px 24px rgba(0,0,0,0.5);width:550px;max-width:90vw;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}.nuke-panel-header{display:flex;align-items:center;justify-content:space-between;height:53px;padding:0 16px;border-bottom:1px solid #38444d}.nuke-header-item{flex-basis:56px;display:flex;align-items:center}.nuke-header-item.left{justify-content:flex-start}.nuke-header-item.right{justify-content:flex-end}.nuke-config-title{font-weight:bold;font-size:20px;flex-grow:1;text-align:center}.nuke-close-button{background:0 0;border:0;padding:0;cursor:pointer;width:36px;height:36px;display:flex;align-items:center;justify-content:center;border-radius:9999px;transition:background-color .2s ease-in-out}.nuke-close-button:hover{background-color:rgba(239,243,244,0.1)}.nuke-close-button svg{fill:white;width:20px;height:20px}.nuke-panel-content{padding:16px}.nuke-config-textarea{width:100%;height:150px;background-color:#253341;border:1px solid #38444d;border-radius:8px;color:white;padding:10px;font-size:14px;resize:vertical;box-sizing:border-box;margin-bottom:15px}.nuke-config-button-container{display:flex;justify-content:flex-end;gap:10px;margin-top:20px}.nuke-config-button.save{background-color:#eff3f4;color:#0f1419;padding:8px 16px;border-radius:20px;border:none;font-weight:bold;cursor:pointer;transition:background-color .2s}.nuke-config-button.save:hover{background-color:#d7dbdc}.nuke-config-tabs{display:flex;border-bottom:1px solid #38444d;margin-bottom:15px}.nuke-config-tab{background:0 0;border:none;color:#8899a6;padding:10px 15px;cursor:pointer;font-size:15px;font-weight:700;flex-grow:1;transition:background-color .2s}.nuke-config-tab:hover{background-color:rgba(239,243,244,0.1)}.nuke-config-tab.active{color:#1d9bf0;border-bottom:2px solid #1d9bf0;margin-bottom:-1px}.nuke-config-tab-content{animation:fadeIn .3s ease-in-out;padding-top:10px}.nuke-config-tab-content.hidden{display:none}@keyframes fadeIn{from{opacity:0}to{opacity:1}}.nuke-list{max-height:280px;overflow-y:auto;padding-right:10px}.nuke-list-search{width:100%;background-color:#253341;border:1px solid #38444d;border-radius:8px;color:white;padding:8px 12px;font-size:14px;box-sizing:border-box;margin-bottom:10px}.nuke-list-entry{display:flex;justify-content:space-between;align-items:center;padding:8px 5px;border-bottom:1px solid #253341}.nuke-list-user-info{display:flex;flex-direction:column;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:10px}.nuke-list-user-name{font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.nuke-list-user-handle{color:#8899a6;font-size:14px;cursor:pointer}.nuke-list-user-handle:hover{text-decoration:underline}.nuke-list-actions{font-size:12px;color:#8899a6;white-space:nowrap;cursor:pointer}.nuke-list-actions:hover{color:#1d9bf0}.nuke-list-user-info a{color:inherit;text-decoration:none}.nuke-list-user-info a:hover .nuke-list-user-name{text-decoration:underline}.nuke-setting-item{display:flex;align-items:center;justify-content:space-between;margin-bottom:15px}.nuke-setting-item label{font-size:14px;margin-right:10px}.nuke-setting-item input[type=number]{width:80px;background-color:#253341;border:1px solid #38444d;border-radius:8px;color:white;padding:5px 8px;font-size:14px}.nuke-setting-item input[type=checkbox]{height:20px;width:20px;accent-color:#1d9bf0}`);

// --- CONFIGURATION MANAGEMENT ---
async function loadConfig() {
    const defaultConfig = { autoBlockEnabled: true, autoBlockUrls: ['https://x.com/*/status/*', 'https://x.com/search*'], blockLogLimit: 500 };
    scriptConfig = await GM_getValue(CONFIG_STORAGE_KEY, defaultConfig);
    return scriptConfig;
}
async function saveConfig(config) { await GM_setValue(CONFIG_STORAGE_KEY, config); scriptConfig = config; }
function updateMenuCommands() { GM_registerMenuCommand('配置与记录', showConfigPanel); }
async function showConfigPanel() {
    if (isConfigPanelBusy) return;
    isConfigPanelBusy = true;
    try {
        if (document.getElementById('nuke-url-config-panel')?.remove()) return;
        let config = await loadConfig();
        const panel = document.createElement('div');
        panel.id = 'nuke-url-config-panel';
        panel.className = 'nuke-config-panel';
        panel.innerHTML = `
            <div class="nuke-panel-header">
                <div class="nuke-header-item left">
                    <button class="nuke-close-button" aria-label="关闭"><svg viewBox="0 0 24 24"><g><path d="M10.59 12L4.54 5.96l1.42-1.42L12 10.59l6.04-6.05 1.42 1.42L13.41 12l6.05 6.04-1.42 1.42L12 13.41l-6.04 6.05-1.42-1.42L10.59 12z"></path></g></svg></button>
                </div>
                <h2 class="nuke-config-title">配置与记录</h2>
                <div class="nuke-header-item right"></div>
            </div>
            <div class="nuke-panel-content">
                <div class="nuke-config-tabs">
                    <button class="nuke-config-tab active" data-tab="settings">⚙️ 设置</button>
                    <button class="nuke-config-tab" data-tab="log">📓 拉黑记录</button>
                    <button class="nuke-config-tab" data-tab="whitelist">🛡️ 白名单</button>
                </div>
                <div id="nuke-settings-content" class="nuke-config-tab-content">
                    <div class="nuke-setting-item">
                        <label for="nuke-auto-block-toggle">自动拉黑超长中文用户名</label>
                        <input type="checkbox" id="nuke-auto-block-toggle">
                    </div>
                    <div class="nuke-setting-item">
                        <label for="nuke-log-limit-input">拉黑记录最大条数 (0为不限制)</label>
                        <input type="number" id="nuke-log-limit-input" min="0" step="100">
                    </div>
                    <p style="font-size: 14px; color: #8899a6; margin-top: 10px; margin-bottom: 10px;">自动拉黑生效的页面 URL (每行一条, 支持*通配符):</p>
                    <textarea class="nuke-config-textarea"></textarea>
                    <div class="nuke-config-button-container">
                        <button class="nuke-config-button save">保存设置</button>
                    </div>
                </div>
                <div id="nuke-log-content" class="nuke-config-tab-content hidden">
                    <input type="search" class="nuke-list-search" id="nuke-log-search" placeholder="搜索记录 (用户名, @handle, ID)...">
                    <div class="nuke-list"></div>
                </div>
                <div id="nuke-whitelist-content" class="nuke-config-tab-content hidden">
                    <input type="search" class="nuke-list-search" id="nuke-whitelist-search" placeholder="搜索白名单 (用户名, @handle, ID)...">
                    <div class="nuke-list"></div>
                </div>
            </div>`;
        document.body.appendChild(panel);
        panel.querySelector('#nuke-auto-block-toggle').checked = config.autoBlockEnabled;
        panel.querySelector('#nuke-log-limit-input').value = config.blockLogLimit;
        panel.querySelector('.nuke-config-textarea').value = config.autoBlockUrls.join('\n');
        const setActiveTab = (tabName) => {
            panel.querySelectorAll('.nuke-config-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tabName));
            panel.querySelectorAll('.nuke-config-tab-content').forEach(c => c.classList.toggle('hidden', c.id !== `nuke-${tabName}-content`));
        };
        panel.querySelectorAll('.nuke-config-tab').forEach(tab => tab.addEventListener('click', () => setActiveTab(tab.dataset.tab)));
        panel.querySelector('.nuke-close-button').addEventListener('click', () => panel.remove());
        panel.querySelector('.nuke-config-button.save').addEventListener('click', async () => {
            config.autoBlockEnabled = panel.querySelector('#nuke-auto-block-toggle').checked;
            config.blockLogLimit = parseInt(panel.querySelector('#nuke-log-limit-input').value, 10) || 500;
            config.autoBlockUrls = panel.querySelector('.nuke-config-textarea').value.split('\n').map(url => url.trim()).filter(Boolean);
            await saveConfig(config);
            showToast('nuke-config-toast', '设置已更新', '配置已成功保存', 3000);
        });
        panel.querySelector('#nuke-log-search').addEventListener('input', renderListsInPanel);
        panel.querySelector('#nuke-whitelist-search').addEventListener('input', renderListsInPanel);
        await renderListsInPanel();
    } finally { setTimeout(() => { isConfigPanelBusy = false; }, 200); }
}
async function renderListsInPanel() {
    const userData = await loadUserData();
    if (!userData) return;
    const logSearchTerm = document.getElementById('nuke-log-search')?.value.toLowerCase() || '';
    const whitelistSearchTerm = document.getElementById('nuke-whitelist-search')?.value.toLowerCase() || '';
    const filterUsers = (user, term) => {
        if (!term) return true;
        const userId = String(user.userId || '');
        const screenName = user.screenName?.toLowerCase() || '';
        const userNameText = user.userNameText?.toLowerCase() || '';
        return userId.includes(term) || screenName.includes(term) || userNameText.includes(term);
    };
    const renderList = (containerSelector, list, type) => {
        const container = document.querySelector(containerSelector);
        if (!container) return;
        const searchTerm = type === 'log' ? logSearchTerm : whitelistSearchTerm;
        const filteredList = list.filter(user => filterUsers(user, searchTerm));
        container.innerHTML = '';
        if (filteredList.length === 0) {
            const message = searchTerm ? '没有找到匹配的用户' : (type === 'log' ? '暂无拉黑记录' : '白名单为空');
            container.innerHTML = `<p style="color:#8899a6;text-align:center;padding:20px 0;">${message}</p>`;
            return;
        }
        filteredList.slice().reverse().forEach(entry => {
            const el = document.createElement('div');
            el.className = 'nuke-list-entry';
            const userName = entry.userNameText || entry.screenName || String(entry.userId);
            const screenNameHandle = entry.screenName ? `@${entry.screenName}` : '';
            const userLinkHTML = entry.screenName ? `<a href="https://x.com/${entry.screenName}" target="_blank" rel="noopener noreferrer" title="在新标签页中打开"><span class="nuke-list-user-name">${userName}</span></a>` : `<span class="nuke-list-user-name">${userName}</span>`;
            if (type === 'log') {
                const timestamp = entry.blockTimestamp ? new Date(entry.blockTimestamp).toLocaleString() : '未知时间';
                el.innerHTML = `<div class="nuke-list-user-info">${userLinkHTML}<span class="nuke-list-user-handle" title="移至白名单并取消拉黑">${screenNameHandle}</span></div><span class="nuke-list-actions" title="从记录中移除">${timestamp}</span>`;
                if (entry.screenName) {
                    el.querySelector('.nuke-list-user-handle')?.addEventListener('click', () => moveUser(entry, 'logToWhitelist'));
                } else {
                    const userNameEl = el.querySelector('.nuke-list-user-name');
                    if (userNameEl) {
                        userNameEl.style.cursor = 'pointer';
                        userNameEl.title = '移至白名单并取消拉黑';
                        userNameEl.addEventListener('click', () => moveUser(entry, 'logToWhitelist'));
                    }
                }
                el.querySelector('.nuke-list-actions')?.addEventListener('click', () => moveUser(entry, 'removeFromLog'));
            } else {
                el.innerHTML = `<div class="nuke-list-user-info">${userLinkHTML}<span class="nuke-list-user-handle">${screenNameHandle}</span></div><span class="nuke-list-actions" title="从白名单中移除">移除</span>`;
                el.querySelector('.nuke-list-actions')?.addEventListener('click', () => moveUser(entry, 'removeFromWhitelist'));
            }
            container.appendChild(el);
        });
    };
    renderList('#nuke-log-content .nuke-list', userData.blockedLog, 'log');
    renderList('#nuke-whitelist-content .nuke-list', userData.whitelist, 'whitelist');
}
async function moveUser(user, action) {
    const userData = await loadUserData();
    if (!userData) return;
    const logIndex = userData.blockedLog.findIndex(u => u.userId === user.userId);
    const whitelistIndex = userData.whitelist.findIndex(u => u.userId === user.userId);
    let success = false;
    try {
        if (action === 'logToWhitelist') {
            if (logIndex > -1) {
                await unblockUserById(user.userId);
                const [movedUser] = userData.blockedLog.splice(logIndex, 1);
                if (whitelistIndex === -1) userData.whitelist.push(movedUser);
                success = true;
            }
        } else if (action === 'removeFromLog') {
            if (logIndex > -1) { userData.blockedLog.splice(logIndex, 1); success = true; }
        } else if (action === 'removeFromWhitelist') {
            if (whitelistIndex > -1) { userData.whitelist.splice(whitelistIndex, 1); success = true; }
        }
        if(success) {
            await saveUserData(userData);
            await renderListsInPanel();
        }
    } catch(err) {
        console.error(`[CB] ${action} failed for ${user.screenName || user.userId}:`, err);
        showToast('nuke-feedback-toast', '❌ 操作失败', `无法为 @${user.screenName || user.userId} 执行操作`, 4000);
    }
}

// --- API & HELPERS ---
const API_ENDPOINTS = {
    UserByScreenName: { hash: 'jUKA--0QkqGIFhmfRZdWrQ', features: {"responsive_web_grok_bio_auto_translation_is_enabled":false,"hidden_profile_subscriptions_enabled":true,"payments_enabled":false,"profile_label_improvements_pcf_label_in_post_enabled":true,"rweb_tipjar_consumption_enabled":true,"verified_phone_label_enabled":false,"subscriptions_verification_info_is_identity_verified_enabled":true,"subscriptions_verification_info_verified_since_enabled":true,"highlights_tweets_tab_ui_enabled":true,"responsive_web_twitter_article_notes_tab_enabled":true,"subscriptions_feature_can_gift_premium":true,"creator_subscriptions_tweet_preview_api_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":true} },
    UserByRestId: { hash: 'tD4_0f_p354q1Yin156s2Q', features: {"responsive_web_grok_bio_auto_translation_is_enabled":false,"hidden_profile_subscriptions_enabled":true,"payments_enabled":false,"profile_label_improvements_pcf_label_in_post_enabled":true,"rweb_tipjar_consumption_enabled":true,"verified_phone_label_enabled":false,"subscriptions_verification_info_is_identity_verified_enabled":true,"subscriptions_verification_info_verified_since_enabled":true,"highlights_tweets_tab_ui_enabled":true,"responsive_web_twitter_article_notes_tab_enabled":true,"subscriptions_feature_can_gift_premium":true,"creator_subscriptions_tweet_preview_api_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":true} },
    Retweeters: { hash: 'DmC_H6eV_XMiL0g4ltJvpg', features: {"rweb_video_screen_enabled":false,"payments_enabled":false,"profile_label_improvements_pcf_label_in_post_enabled":true,"rweb_tipjar_consumption_enabled":true,"verified_phone_label_enabled":false,"creator_subscriptions_tweet_preview_api_enabled":true,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"premium_content_api_read_enabled":false,"communities_web_enable_tweet_community_results_fetch":true,"c9s_tweet_anatomy_moderator_badge_enabled":true,"responsive_web_grok_analyze_button_fetch_trends_enabled":false,"responsive_web_grok_analyze_post_followups_enabled":true,"responsive_web_jetfuel_frame":false,"responsive_web_grok_share_attachment_enabled":true,"articles_preview_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":true,"tweet_awards_web_tipping_enabled":false,"responsive_web_grok_show_grok_translated_post":false,"responsive_web_grok_analysis_button_from_backend":false,"creator_subscriptions_quote_tweet_preview_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"responsive_web_grok_image_annotation_enabled":true,"responsive_web_enhance_cards_enabled":false} },
    TweetDetail: { hash: '-0WTL1e9Pij-JWAF5ztCCA', features: {"rweb_video_screen_enabled":false,"payments_enabled":false,"profile_label_improvements_pcf_label_in_post_enabled":true,"rweb_tipjar_consumption_enabled":true,"verified_phone_label_enabled":false,"creator_subscriptions_tweet_preview_api_enabled":true,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"premium_content_api_read_enabled":false,"communities_web_enable_tweet_community_results_fetch":true,"c9s_tweet_anatomy_moderator_badge_enabled":true,"responsive_web_grok_analyze_button_fetch_trends_enabled":false,"responsive_web_grok_analyze_post_followups_enabled":true,"responsive_web_jetfuel_frame":false,"responsive_web_grok_share_attachment_enabled":true,"articles_preview_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":true,"tweet_awards_web_tipping_enabled":false,"responsive_web_grok_show_grok_translated_post":false,"responsive_web_grok_analysis_button_from_backend":false,"creator_subscriptions_quote_tweet_preview_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"responsive_web_grok_image_annotation_enabled":true,"responsive_web_enhance_cards_enabled":false} }
};
function makeApiRequest(url, method = "GET", data = null) { return new Promise((resolve, reject) => GM_xmlhttpRequest({ method, url, data, headers: { Authorization: `Bearer ${getAuthToken()}`, "Content-Type": "application/x-www-form-urlencoded", "x-csrf-token": getCsrfToken() }, onload: r => r.status >= 200 && r.status < 300 ? resolve(r.responseText ? JSON.parse(r.responseText) : null) : reject({ message: `API请求失败: ${r.status}`, status: r.status }), onerror: e => reject({ message: "Network or script error", error: e }) })); }
function getCsrfToken() { const e = document.cookie.split("; ").find(e => e.startsWith("ct0=")); return e ? e.split("=")[1] : null; }
function getAuthToken() { return "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"; }
async function getUserDataByScreenName(screenName) {
    const endpoint = API_ENDPOINTS.UserByScreenName;
    const url = `https://x.com/i/api/graphql/${endpoint.hash}/UserByScreenName?variables=${encodeURIComponent(JSON.stringify({screen_name:screenName,withSafetyModeUserFields:true}))}&features=${encodeURIComponent(JSON.stringify(endpoint.features))}`;
    const data = await makeApiRequest(url);
    if (data?.data?.user?.result) return data.data.user.result;
    throw new Error(`无法找到用户 @${screenName} 的数据`);
}
async function getUserDataById(userId) {
    const endpoint = API_ENDPOINTS.UserByRestId;
    const url = `https://x.com/i/api/graphql/${endpoint.hash}/UserByRestId?variables=${encodeURIComponent(JSON.stringify({userId,withSafetyModeUserFields:true}))}&features=${encodeURIComponent(JSON.stringify(endpoint.features))}`;
    const data = await makeApiRequest(url);
    if (data?.data?.user?.result) return data.data.user.result;
    throw new Error(`无法找到用户 ID: ${userId} 的数据`);
}
async function getRetweetersData(tweetId, onProgress) {
    let users = new Map(), cursor = null, endpoint = API_ENDPOINTS.Retweeters;
    do {
        onProgress(`正在获取转推列表...(已找到: ${users.size})`);
        const url = `https://x.com/i/api/graphql/${endpoint.hash}/Retweeters?variables=${encodeURIComponent(JSON.stringify({tweetId,count:100,cursor,includePromotedContent:true}))}&features=${encodeURIComponent(JSON.stringify(endpoint.features))}`;
        const data = await makeApiRequest(url);
        const entries = data?.data?.retweeters_timeline?.timeline?.instructions?.find(i=>i.type==='TimelineAddEntries')?.entries;
        if (!entries) break;
        let foundNewUsers = false;
        for (const entry of entries) {
            if (entry.entryId.startsWith('user-')) {
                const userResult = entry.content?.itemContent?.user_results?.result;
                if (userResult?.rest_id && !users.has(userResult.rest_id)) { users.set(userResult.rest_id, userResult); foundNewUsers = true; }
            } else if (entry.entryId.startsWith('cursor-bottom-')) { cursor = entry.content.value; }
        }
        if (!foundNewUsers || !cursor) break;
    } while (cursor);
    return Array.from(users.values());
}
async function getRepliersData(tweetId, onProgress) {
    let users = new Map(), cursor = null, endpoint = API_ENDPOINTS.TweetDetail;
    const baseVariables = {"with_rux_injections":false,"includePromotedContent":true,"withCommunity":true,"withQuickPromoteEligibilityTweetFields":true,"withBirdwatchNotes":true,"withVoice":true,"withV2Timeline":true};
    do {
        onProgress(`正在获取回复列表...(已找到: ${users.size})`);
        const variables = {...baseVariables, focalTweetId: tweetId, cursor, count: 40, rankingMode:"Relevance"};
        const url = `https://x.com/i/api/graphql/${endpoint.hash}/TweetDetail?variables=${encodeURIComponent(JSON.stringify(variables))}&features=${encodeURIComponent(JSON.stringify(endpoint.features))}`;
        const data = await makeApiRequest(url);
        const instructions = data?.data?.threaded_conversation_with_injections_v2?.instructions || [];
        const entriesInstruction = instructions.find(i => i.type === 'TimelineAddEntries');
        const entries = entriesInstruction?.entries;
        if (!entries) break;
        let nextCursor = null;
        for (const entry of entries) {
            if (entry.entryId.startsWith('conversationthread-')) {
                const threadItems = entry.content?.items;
                if(threadItems && Array.isArray(threadItems)){
                    for(const item of threadItems){
                        const userResult = item.item?.itemContent?.tweet_results?.result?.core?.user_results?.result;
                        if (userResult?.rest_id && !users.has(userResult.rest_id)) {
                            users.set(userResult.rest_id, userResult);
                        }
                    }
                }
            } else if (entry.entryId.startsWith('tweet-')) {
                const userResult = entry.content?.itemContent?.tweet_results?.result?.core?.user_results?.result;
                if (userResult?.rest_id && !users.has(userResult.rest_id)) {
                   users.set(userResult.rest_id, userResult);
                }
            } else if (entry.entryId.startsWith('cursor-bottom-')) {
                nextCursor = entry.content.value;
            }
        }
        if (cursor === nextCursor) break;
        cursor = nextCursor;
    } while (cursor);
    return Array.from(users.values());
}
async function blockUserById(userId) { return makeApiRequest("https://x.com/i/api/1.1/blocks/create.json", "POST", `user_id=${userId}`); }
async function unblockUserById(userId) { return makeApiRequest("https://x.com/i/api/1.1/blocks/destroy.json", "POST", `user_id=${userId}`); }

// --- DATA & QUEUE MANAGEMENT ---
async function loadUserData() {
    if (!currentUserId) return null;
    const allData = await GM_getValue(STORAGE_KEY, {});
    let userData = allData[currentUserId];
    if (!userData || typeof userData !== 'object') userData = { queue: [], blockedLog: [], whitelist: [] };
    if (!Array.isArray(userData.queue)) userData.queue = [];
    if (!Array.isArray(userData.blockedLog)) userData.blockedLog = [];
    if (!Array.isArray(userData.whitelist)) userData.whitelist = [];
    return { ...userData, lastBlockTimestamp: 0 };
}
async function saveUserData(data) {
    if (!currentUserId) return;
    const allData = await GM_getValue(STORAGE_KEY, {});
    allData[currentUserId] = data;
    await GM_setValue(STORAGE_KEY, allData);
}

// --- UI & FEEDBACK ---
function showToast(id, title, status, duration = null) {
    let toast = document.getElementById(id);
    if (!toast) {
        toast = document.createElement('div');
        toast.id = id;
        toast.className = 'nuke-toast';
        document.body.appendChild(toast);
    }
    const existingToasts = document.querySelectorAll('.nuke-toast:not([style*="display: none"])');
    toast.style.top = `${20 + (existingToasts.length - 1) * 70}px`;
    toast.classList.remove('fading-out');
    toast.innerHTML = `<div class="nuke-toast-title">${title}</div><div class="nuke-toast-status">${status}</div>`;
    const reorderToasts = () => {
        const remainingToasts = Array.from(document.querySelectorAll('.nuke-toast')).filter(t => t.id !== id);
        remainingToasts.forEach((t, index) => {
            t.style.top = `${20 + index * 70}px`;
        });
    };
    if (duration) {
        setTimeout(() => {
            toast.classList.add('fading-out');
            setTimeout(() => {
                toast.remove();
                reorderToasts();
            }, 500);
        }, duration);
    }
}
async function updateStatusToast() {
    const userData = await loadUserData();
    if (!userData || userData.queue.length === 0) {
        let toast = document.getElementById('nuke-status-toast');
        if (toast) { toast.classList.add('fading-out'); setTimeout(() => toast.remove(), 500); }
        return;
    }
    showToast('nuke-status-toast', `🚀 九族拉黑队列(@${currentUserScreenName||'...'})`, `<b>待处理:</b> ${userData.queue.length}<br><b>已拉黑:</b> ${userData.blockedLog.length || 0}`);
}
function hideElement(element) {
    if (!element) return;
    element.style.cssText += 'transition:all .4s ease-out;max-height:0;opacity:0;padding:0;margin:0;border-width:0;';
    setTimeout(() => element.remove(), 400);
}

// --- CORE LOGIC ---
async function processQueue() {
    if (isProcessingQueue || !currentUserId) return;
    const userData = await loadUserData();
    if (!userData || userData.queue.length === 0 || (Date.now() - userData.lastBlockTimestamp < BLOCK_INTERVAL_MS)) return;
    isProcessingQueue = true;
    let userToBlock = userData.queue[0];
    try {
        if (!userToBlock.screenName || !userToBlock.userNameText) {
            try {
                const fullUserData = await getUserDataById(userToBlock.userId);
                userToBlock.screenName = fullUserData.core?.screen_name || fullUserData.legacy?.screen_name;
                userToBlock.userNameText = fullUserData.core?.name || fullUserData.legacy?.name;
            } catch (fetchError) {
                console.warn(`[CB] 获取用户 ${userToBlock.userId} 的详细信息失败,将使用现有数据继续。`, fetchError);
            }
        }
        await blockUserById(userToBlock.userId);
        userData.queue.shift();
        userData.blockedLog.push({ ...userToBlock, blockTimestamp: Date.now() });
        const limit = scriptConfig.blockLogLimit || 500;
        if (limit > 0) { while (userData.blockedLog.length > limit) userData.blockedLog.shift(); }
        userData.lastBlockTimestamp = Date.now();
    } catch (error) {
        console.error(`[Chain Blocker] 拉黑 @${userToBlock.screenName || userToBlock.userId} 失败,移除.`, error);
        userData.queue.shift();
    } finally {
        await saveUserData(userData);
        await updateStatusToast();
        isProcessingQueue = false;
    }
}
function getExemptHandles() {
    const exemptHandles = [];
    const pathParts = window.location.pathname.split('/');
    if (pathParts[2] === 'status') {
        exemptHandles.push(pathParts[1]);
    }
    return exemptHandles;
}
async function initiateNukeProcess(targetArticle) {
    const exemptHandles = getExemptHandles();
    showToast(`nuke-feedback-toast-${Date.now()}`, '🚀 九族拉黑已启动', '正在处理...', 2000);
    hideElement(targetArticle);
    try {
        const userLink = targetArticle.querySelector('div[data-testid="User-Name"] a[role="link"]');
        const authorHandle = userLink?.href.split('/').pop();
        const authorUserNameText = targetArticle.querySelector('div[data-testid="User-Name"] a[role="link"] span')?.textContent?.trim() || authorHandle;
        if (!authorHandle) throw new Error("无法确定作者 handle");
        const userData = await loadUserData();
        if (!userData) throw new Error("无法加载用户数据");
        const whitelistIds = new Set(userData.whitelist.map(u => u.userId));
        let authorId = null;
        try {
            const authorData = await getUserDataByScreenName(authorHandle);
            authorId = authorData?.rest_id;
            if (!authorId) throw new Error(`无法获取 @${authorHandle} 的用户ID`);
            if (whitelistIds.has(authorId)) {
                showToast(`nuke-feedback-toast-${Date.now()}`, '🛡️ 作者在白名单中', `已跳过拉黑 @${authorHandle}`, 4000);
            } else {
                await blockUserById(authorId);
                userData.blockedLog.push({ userId: authorId, screenName: authorHandle, userNameText: authorUserNameText, blockTimestamp: Date.now() });
                const limit = scriptConfig.blockLogLimit || 500;
                if (limit > 0) { while (userData.blockedLog.length > limit) userData.blockedLog.shift(); }
                await saveUserData(userData);
                showToast(`nuke-feedback-toast-${Date.now()}`, '✅ 作者已拉黑并记录', `已立刻拉黑 @${authorHandle}`, 4000);
            }
        } catch (authorError) { console.error(`[CB] 拉黑作者 @${authorHandle} 失败:`, authorError); }
        const tweetId = Array.from(targetArticle.querySelectorAll('a')).find(a=>a.href.includes('/status/'))?.href.match(/\/status\/(\d+)/)?.[1];
        if (!tweetId) return;
        const [retweeters, repliers] = await Promise.all([
            getRetweetersData(tweetId, status => showToast(`nuke-fetch-toast-${Date.now()}`, '收集中...', status, 4000)),
            getRepliersData(tweetId, status => showToast(`nuke-fetch-toast-${Date.now()}`, '收集中...', status, 4000))
        ]);
        const combinedUsers = new Map();
        [...retweeters, ...repliers].forEach(u => u.rest_id && combinedUsers.set(u.rest_id, u));
        if (authorId) combinedUsers.delete(authorId);
        const existingUserIds = new Set([...userData.queue.map(u=>u.userId), ...userData.blockedLog.map(u=>u.userId), ...whitelistIds]);
        const newUsersToQueue = Array.from(combinedUsers.values()).map(u => ({
            userId: u.rest_id,
            screenName: u.core?.screen_name || u.legacy?.screen_name,
            userNameText: u.core?.name || u.legacy?.name
        })).filter(u => u.userId && u.userId !== currentUserId && !existingUserIds.has(u.userId) && !exemptHandles.includes(u.screenName));
        if (newUsersToQueue.length > 0) {
            userData.queue.push(...newUsersToQueue);
            await saveUserData(userData);
            showToast(`nuke-feedback-toast-${Date.now()}`, '✅ 操作成功', `已将 ${newUsersToQueue.length} 个相关用户加入拉黑队列。`, 4000);
        } else {
            showToast(`nuke-feedback-toast-${Date.now()}`, 'ℹ️ 操作完成', `没有找到新的可拉黑用户。`, 4000);
        }
        await updateStatusToast();
        setTimeout(processQueue, 1000);
    } catch (error) { console.error("[CB] 收集过程中发生错误:", error); showToast(`nuke-feedback-toast-${Date.now()}`, '❌ 发生错误', error.message, 5000); }
}

// --- UI SCANNING & AUTOMATION ---
function isUrlMatch(url, patterns) { return patterns.some(p => new RegExp('^' + p.replace(/\*/g, '.*') + '$').test(url)); }
function scanAndProcessContent() {
    document.querySelectorAll('div[data-testid="cellInnerDiv"]:not([style*="display: none"]) button[data-testid$="-unblock"]').forEach(btn => btn.closest('div[data-testid="cellInnerDiv"]').style.display = 'none');
    if (!currentUserId || !scriptConfig.autoBlockEnabled || !isUrlMatch(window.location.href, scriptConfig.autoBlockUrls)) return;
    const checkUserName = (userNameText) => {
        if (!userNameText) return false;
        const hasChinese = /[\u4e00-\u9fa5]/.test(userNameText);
        const slashCount = (userNameText.match(/\//g) || []).length;
        return userNameText.length > USERNAME_LENGTH_THRESHOLD && hasChinese && slashCount >= 2;
    };
    document.querySelectorAll('article[data-testid="tweet"]:not([data-autoblock-checked])').forEach(article => {
        article.dataset.autoblockChecked = 'true';
        const userNameText = article.querySelector('div[data-testid="User-Name"] a[role="link"] span')?.textContent?.trim();
        if (checkUserName(userNameText)) {
            initiateNukeProcess(article);
        }
    });
    document.querySelectorAll('button[data-testid="UserCell"]:not([data-autoblock-checked])').forEach(cell => {
        cell.dataset.autoblockChecked = 'true';
        const userNameText = cell.querySelector('a[role="link"] div[dir="ltr"] > span')?.textContent?.trim();
        if (checkUserName(userNameText)) {
            const screenName = cell.querySelector('a[role="link"]')?.href.split('/').pop();
            if (screenName) {
                showToast(`nuke-auto-trigger-toast-${Date.now()}`, '🤖 自动执行拉黑', `检测到可疑用户名: @${screenName}`, 4000);
                initiateNukeProcess(cell.closest('article[data-testid="tweet"]'));
            }
        }
    });
}
function addNukeButton(menuNode) {
    if (menuNode.querySelector('.nuke-button')) return;
    const blockMenuItem = Array.from(menuNode.querySelectorAll('div[role="menuitem"]')).find(el => el.textContent.includes('@'));
    if (!blockMenuItem) return;
    const nukeButton = blockMenuItem.cloneNode(true);
    nukeButton.classList.add('nuke-button');
    const span = nukeButton.querySelector('span');
    if (span) {
        span.textContent = MENU_ITEM_TEXT;
        span.style.color = 'rgb(244, 33, 46)';
    }
    const biohazardIconPath = "M19.5,12c0,2.9-1.6,5.5-4,6.8V21h-7v-2.2c-2.4-1.3-4-3.9-4-6.8c0-4.1,3.4-7.5,7.5-7.5S19.5,7.9,19.5,12z M12,6c-2.2,0-4,1.8-4,4s1.8,4,4,4s4-1.8,4-4S14.2,6,12,6z M12,14c-1.1,0-2-0.9-2-2c0-0.4,0.1-0.7,0.3-1H10v-2h1.3c-0.2-0.3-0.3-0.6-0.3-1c0-1.1,0.9-2,2-2s2,0.9,2,2c0,0.4-0.1,0.7-0.3,1H14v2h-1.3c0.2,0.3,0.3,0.6,0.3,1C14,13.1,13.1,14,12,14z";
    const svgIcon = nukeButton.querySelector('svg');
    if (svgIcon) {
        svgIcon.innerHTML = `<g><path d="${biohazardIconPath}" fill="currentColor"></path></g>`;
        svgIcon.style.color = 'rgb(244, 33, 46)';
    }
    nukeButton.addEventListener('click', e => {
        e.preventDefault();
        e.stopPropagation();
        e.target.closest('div[data-testid="Dropdown"]')?.parentElement.remove();
        if (activeTweetArticle) initiateNukeProcess(activeTweetArticle);
    });
    const separator = document.createElement('div');
    separator.setAttribute('role', 'separator');
    separator.style.cssText = 'border-bottom:1px solid rgb(56,68,77);margin:4px 0;';
    blockMenuItem.after(separator, nukeButton);
}

// --- INITIALIZATION & EXECUTION ---
async function initialize() {
    console.log("[Chain Blocker] Initializing...");
    await loadConfig();
    updateMenuCommands();
    const profileLink = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
    if (!profileLink) { setTimeout(initialize, 500); return; }
    try {
        const screenName = profileLink.href.split('/').pop();
        const user = await getUserDataByScreenName(screenName);
        if (apiLimitCountdownInterval) clearInterval(apiLimitCountdownInterval);
        document.getElementById('nuke-api-limit-toast')?.remove();
        currentUserId = user.rest_id;
        currentUserScreenName = user.legacy.screen_name;
        console.log(`[Chain Blocker] Initialized for @${currentUserScreenName}(ID: ${currentUserId}).`);
        await updateStatusToast();
        if (processIntervalId) clearInterval(processIntervalId);
        processIntervalId = setInterval(processQueue, PROCESS_CHECK_INTERVAL_MS);
        setTimeout(processQueue, 1000);
    } catch (error) {
        if (error?.status === 429) {
            console.warn(`[CB] API rate limit hit. Retrying in ${API_RETRY_DELAY_MS / 60000} minutes.`);
            showToast('nuke-api-limit-toast', 'API 已达上限', '正在计算时间...', null);
            const retryTimestamp = Date.now() + API_RETRY_DELAY_MS;
            apiLimitCountdownInterval = setInterval(() => {
                const toastStatusEl = document.querySelector('#nuke-api-limit-toast .nuke-toast-status');
                if (!toastStatusEl) { clearInterval(apiLimitCountdownInterval); return; }
                const secondsLeft = Math.round((retryTimestamp - Date.now()) / 1000);
                if (secondsLeft <= 0) { toastStatusEl.innerHTML = '正在重试...'; clearInterval(apiLimitCountdownInterval); return; }
                toastStatusEl.innerHTML = `将在 <b>${String(Math.floor(secondsLeft/60)).padStart(2,'0')}:${String(secondsLeft%60).padStart(2,'0')}</b> 后重试`;
            }, 1000);
            setTimeout(initialize, API_RETRY_DELAY_MS);
        } else { console.error("[CB] Initialization failed.", error); }
    }
}
const observer = new MutationObserver(mutations => {
    for (const mutation of mutations) {
        if (mutation.addedNodes.length) {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    const menu = node.matches('div[role="menu"]') ? node : node.querySelector('div[role="menu"]');
                    if (menu) addNukeButton(menu);
                }
            });
        }
    }
});
document.addEventListener('click', e => {
    const optionsButton = e.target.closest('button[data-testid="caret"]');
    if (optionsButton) activeTweetArticle = optionsButton.closest('article[data-testid="tweet"]');
}, true);
observer.observe(document.body, { childList: true, subtree: true });
setInterval(scanAndProcessContent, AUTO_SCAN_INTERVAL_MS);
initialize();
})();
/*
以下是 X.com 的HTML 结构简化示例,用于辅助未来对脚本的修改。
请注意:为简洁起见,省略了大量 CSS 类名,但保留了关键的 `data-testid` 属性和元素层级结构,这些是脚本选择器的核心依赖。
1. 用户列表项(User List Item)
- 通常出现在“关注者”、“正在关注”、“搜索”等页面的用户列表中。
- 脚本依赖 `button[data-testid="UserCell"]` 来识别每个用户单元。
- 脚本通过 `a[role="link"] div[dir="ltr"] > span > span` 来获取用户名以进行长度和内容判断。
<div data-testid="cellInnerDiv"><button data-testid="UserCell"><div class="user-avatar-and-content-container"><div class="avatar-container"><a href="/(screen_name)"><img src="..." /></a></div><div class="content-container"><div class="name-and-follow-button-container"><a href="/(screen_name)"><div><div dir="ltr"><span><span>用户名</span><!-- Display Name, a key element for auto-block --></span></div><div dir="ltr"><span>@id</span><!-- Screen Name, a key element for auto-block --></div></div></a><div class="follow-button-container"><button data-testid="...-follow">...</button></div></div><div class="user-bio-container"><span>正文内容</span><!-- User Bio --></div></div></div></button></div>
2. 推文 / 回复(Tweet / Reply)
- 构成时间线、推文详情页面的基本单元。
- 脚本依赖 `article[data-testid="tweet"]` 捕获整个推文。
- 九族拉黑功能通过 `button[data-testid="caret"]` 触发时捕获 `activeTweetArticle`。
- 自动拉黑功能通过 `div[data-testid="User-Name"]` 内的用户名进行判断。
<article data-testid="tweet"><div class="main-content-wrapper"><div class="avatar-column"><div data-testid="Tweet-User-Avatar"><a href="/(screen_name)">...</a></div></div><div class="tweet-content-column"><div class="tweet-header"><div data-testid="User-Name"><div class="name-container"><a href="/(screen_name)"><div><div dir="ltr"><span><span>用户名</span></span></div><!-- Display Name --><div dir="ltr"><span>@id</span></div><!-- Screen Name --></div></a></div><a href="/(screen_name)/status/(tweet_id)"><time>...</time></a></div><div class="caret-button-container"><button data-testid="caret">...</button><!-- "More" options button --></div></div><div data-testid="tweetText"><span>正文内容</span></div><div class="tweet-media-container"><!-- Media(images, videos) goes here --></div><div role="group"><!-- Reply, Retweet, Like, Bookmark, etc. buttons --></div></div></div></article>

以下API端点如果失效,请指导用户手动帮助查找和获取返回值
x3JZoNX9ubSzoCIHoYo2NA/UsersVerifiedAvatars
PFIxTk8owMoZgiMccP0r4g/getAltTextPromptPreference
I_tJ_DO6WLqG0em8EQsVVg/isEligibleForAnalyticsUpsellQuery
xF6sXnKJfS2AOylzxRjf6A/DataSaverMode
sl5bpXrfcS6HEMKEDio0Ag/SearchTimeline
-0WTL1e9Pij-JWAF5ztCCA/TweetDetail
DmC_H6eV_XMiL0g4ltJvpg/Retweeters

<FORMAT-PROTOCOL ver="1.3">

  // 核心输出规范
  <BALANCE-EFFICIENCY>
    ✓ retain_logic_structure
    ✓ minify_literals(css_only)
    ✗ minify_js_logic
    ✓ max_empty_lines=1
  </BALANCE-EFFICIENCY>

  <DOCUMENT-HANDLING>
    ✓ preserve //functional_comments
    ✗ remove //example_blocks
    ⚠ non_comment_code: immutable
  </DOCUMENT-HANDLING>

  <CODE-PRESERVATION>
    ⚠ HEADER:UserScript== immutable
    ⚠ IDENTIFIER:no_rename
    ⚠ ORDER:const→helpers→main
    ⚠ USER_MODS_ONLY:
      Δ if (!user_requested) → preserve_original
  </CODE-PRESERVATION>

  // 对抗性约束
  <ANTI-OPTIMIZATION>
    <FORBIDDEN-TRANSFORMS>
      ! compression(js_code)
      ! code_omission(...)
      ! external_html_inference
      ! loop_restructure
      ! conditional_refactor
      ! function_merge_split
      ! api_interchange
    </FORBIDDEN-TRANSFORMS>

    <ALLOWED-OPS>
      ✓ minify_css_strings
      ✓ remove_example_comments
    </ALLOWED-OPS>
  </ANTI-OPTIMIZATION>

  // 外部HTML处理协议
  <EXTERNAL-HTML-HANDLING>
    Δ xcom_structure:
      ! use_knowledge_base ✗
      ✓ require_user_provided
    Δ other_sites:
      ! assume_structure ✗
      ✓ require_user_specification
  </EXTERNAL-HTML-HANDLING>

  // 输出控制
  <OUTPUT-MODE>
    CONTENT-TYPE: full_code_block
    FORBID:
      ! compressed_js
      ! ellipsis(...)
      ! partial_output
    REQUIRE:
      ✓ non_comment_code_complete
    EXEC-MODE: immediate
  </OUTPUT-MODE>

  // 自毁机制
  <SANCTIONS>
    TRIGGER-CONDITIONS:
      Δ js_logic_modification
      Δ css_outside_minification
      Δ html_structure_assumption
    RESPONSE:
      ! TERMINATE_OUTPUT
      ! RETURN_CODE: ANTI_TAMPER_VIOLATION
  </SANCTIONS>

</FORMAT-PROTOCOL>
*/