Greasy Fork

Greasy Fork is available in English.

X(Twitter) 自动取关助手

自动取关 X(Twitter)

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         X(Twitter) 自动取关助手
// @namespace    http://tampermonkey.net/
// @version      3.2
// @description  自动取关 X(Twitter)
// @author       Ri
// @match        https://twitter.com/*/following
// @match        https://x.com/*/following
// @grant        none
// ==/UserScript==

(function() {
  'use strict';

  // ---------------- global state ----------------
  let unfollowCount = 0;
  let maxUnfollows = 200;
  let onlyNotFollowingBack = true;
  let running = false;
  let paused = false;
  let delayTime = 2000; // 毫秒

  // ---------------- load web fonts (Poppins 主体, Pacifico 广告) ----------------
  function loadFonts() {
    try {
      const href = 'https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&family=Pacifico&display=swap';
      const link = document.createElement('link');
      link.rel = 'stylesheet';
      link.href = href;
      document.head.appendChild(link);
    } catch (e) {
      console.warn('加载字体失败', e);
    }
  }
  loadFonts();

  // ---------------- util ----------------
  function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
  function now() { return new Date().toLocaleTimeString(); }

  // ---------------- core unfollow loop ----------------
  async function autoUnfollow() {
    logMessage('✅ 脚本启动');
    while (running && unfollowCount < maxUnfollows) {
      if (paused) {
        await sleep(1000);
        continue;
      }

      // 查找按钮:保留你在控制台测试通过的简单策略(按钮文本匹配)
      const unfollowButtons = [...document.querySelectorAll('button')]
        .filter(btn => {
          const txt = (btn.innerText || '').trim();
          return ["フォロー中", "Following", "已关注", "正在关注"].includes(txt);
        });

      if (unfollowButtons.length === 0) {
        logMessage('⚠ 未找到可用按钮,滚动加载更多...');
        window.scrollBy(0, 1200);
        await sleep(2000);
        continue;
      }

      for (const btn of unfollowButtons) {
        if (!running || unfollowCount >= maxUnfollows) break;
        if (paused) break;

        // 判断是否是同一个主列项(尽量使用你原始脚本里成功的 cellInnerDiv)
        const userBlock = btn.closest('div[data-testid="cellInnerDiv"]');
        const textContent = userBlock ? userBlock.innerText : '';

        if (onlyNotFollowingBack &&
            (textContent.includes("フォローされています") ||
             textContent.includes("Follows you") ||
             textContent.includes("正在关注你") ||
             textContent.includes("关注了你"))) {
          logMessage('➡ 跳过:已回关');
          continue;
        }

        // 点击“フォロー中 / Following”
        try {
          btn.click();
          await sleep(700);
        } catch (e) {
          logMessage('⚠ 点击按钮失败:' + (e.message || e));
          continue;
        }

        // 点击确认按钮(多语言)
        const confirmBtn = [...document.querySelectorAll('button')]
          .find(b => ["フォロー解除","Unfollow","取消关注"].some(t => (b.innerText || '').includes(t)));

        if (confirmBtn) {
          try {
            confirmBtn.click();
            unfollowCount++;
            logMessage(`✅ 已取关 ${unfollowCount} / ${maxUnfollows}`);
            updateProgress();
          } catch (e) {
            logMessage('⚠ 点击确认失败:' + (e.message || e));
          }
        } else {
          logMessage('⚠ 未找到确认按钮(已跳过)');
        }

        // 随机或固定延迟(这里使用固定 delayTime)
        await sleep(delayTime);
      }

      // 向下滚动加载更多
      window.scrollBy(0, 1200);
      await sleep(1200);
    }

    running = false;
    paused = false;
    updateButtonStates('stop');
    logMessage(`🎯 任务结束:共处理 ${unfollowCount} 人`);
    try { alert(`任务完成,共处理 ${unfollowCount} 人`); } catch(e){}
  }

  // ---------------- UI panel ----------------
  function createPanel() {
    if (document.getElementById('xauto-panel-v3')) return;
    const panel = document.createElement('div');
    panel.id = 'xauto-panel-v3';
    Object.assign(panel.style, {
      position: 'fixed',
      top: '80px',
      right: '20px',
      width: '300px',
      zIndex: 2147483647,
      boxSizing: 'border-box',
      fontFamily: "Poppins, 'Segoe UI', Roboto, Arial, sans-serif",
      fontSize: '14px'
    });

    panel.innerHTML = `
      <div style="background:#fff;border-radius:12px;box-shadow:0 6px 20px rgba(2,6,23,.25);padding:12px;border:1px solid rgba(0,0,0,0.06);">
        <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
          <div style="font-weight:600;font-size:16px;color:#111">🚀 自动取关助手</div>
          <div style="font-size:12px;color:#6b7280">v3.2</div>
        </div>

        <div style="margin-bottom:8px;">
          <label style="color:#374151;">数量上限:
            <input id="maxUnf" type="number" value="${maxUnfollows}" min="1" style="width:90px;padding:6px;border-radius:6px;border:1px solid #e5e7eb;text-align:center;margin-left:6px;">
          </label>
        </div>

        <div style="display:flex;gap:8px;align-items:center;margin-bottom:8px;">
          <label style="flex:1;color:#374151;"><input id="onlyNotBack" type="checkbox" ${onlyNotFollowingBack? 'checked':''}> 仅未回关者</label>
          <label style="color:#374151;">间隔(秒):
            <input id="delayTime" type="number" value="${delayTime/1000}" min="1" max="60" style="width:60px;padding:6px;border-radius:6px;border:1px solid #e5e7eb;text-align:center;margin-left:6px;">
          </label>
        </div>

        <div style="display:flex;gap:8px;margin-bottom:10px;">
          <button id="startUnf" style="flex:1;padding:8px;border-radius:8px;border:0;background:#16a34a;color:#fff;font-weight:600;cursor:pointer;">▶ 开始</button>
          <button id="pauseUnf" style="flex:1;padding:8px;border-radius:8px;border:0;background:#f59e0b;color:#fff;font-weight:600;cursor:pointer;" disabled>⏸ 暂停</button>
          <button id="stopUnf" style="flex:1;padding:8px;border-radius:8px;border:0;background:#ef4444;color:#fff;font-weight:600;cursor:pointer;" disabled>⏹ 停止</button>
        </div>

        <div style="margin-bottom:8px;color:#374151;font-size:13px;">进度:<span id="progressText">0 / ${maxUnfollows}</span></div>
        <div style="background:#f3f4f6;border-radius:8px;height:10px;overflow:hidden;margin-bottom:10px;">
          <div id="progressBar" style="width:0%;height:100%;background:linear-gradient(90deg,#10b981,#059669);"></div>
        </div>

        <div id="logBox" style="height:120px;overflow:auto;background:#fbfeff;border:1px solid #eef2ff;padding:8px;border-radius:8px;font-size:12px;color:#374151;">
          ${now()} - 面板就绪,请设置后点击开始。
        </div>

        <div id="xauto-ad" style="margin-top:10px;border-radius:8px;padding:10px;color:#fff;text-align:center;background:linear-gradient(90deg,#ff416c,#ff4b2b);">
          <div style="font-weight:700;font-size:14px;">覆盖海内外各大电商短视频平台</div>
          <div style="margin:6px 0;font-size:13px;">粉丝|点赞|评论|分享|收藏|播放|直播人气|电商引流</div>
          <div style="font-weight:700;font-size:13px;margin-bottom:6px;">业务项目超900+,全网独家货源</div>
          <div style="font-size:12px;margin-bottom:8px;">/ 安全稳定 · 自助下单 /</div>
          <a href="https://hdwx.wstop.top/" target="_blank" style="display:inline-block;padding:8px 12px;background:#fff;color:#ff4b2b;border-radius:8px;font-weight:700;text-decoration:none;">👉 点击进入</a>
        </div>
      </div>
    `;

    document.body.appendChild(panel);

    // 强制设置广告字体(高优先级)
    const ad = document.getElementById('xauto-ad');
    try {
      ad.style.setProperty('font-family', "Pacifico, 'Trebuchet MS', 'Comic Sans MS', sans-serif", 'important');
    } catch (e) { /* ignore */ }

    // 按钮 & 控件
    const startBtn = document.getElementById('startUnf');
    const pauseBtn = document.getElementById('pauseUnf');
    const stopBtn = document.getElementById('stopUnf');
    const maxInput = document.getElementById('maxUnf');
    const delayInput = document.getElementById('delayTime');
    const onlyNotBackInput = document.getElementById('onlyNotBack');

    // 初始化进度显示
    maxUnfollows = parseInt(maxInput.value) || maxUnfollows;
    updateProgress();

    // 监听上限实时变化(立即更新进度显示)
    maxInput.addEventListener('change', () => {
      maxUnfollows = parseInt(maxInput.value) || 1;
      updateProgress();
    });

    // 监听间隔输入变化(动态生效,下次开始或继续生效)
    delayInput.addEventListener('change', () => {
      const v = parseInt(delayInput.value);
      if (!isNaN(v) && v >= 1) delayTime = v * 1000;
    });

    // 监听仅未回关切换
    onlyNotBackInput.addEventListener('change', () => {
      onlyNotFollowingBack = !!onlyNotBackInput.checked;
    });

    // 按钮行为
    startBtn.addEventListener('click', (e) => {
      if (running) return;
      unfollowCount = 0;
      maxUnfollows = parseInt(maxInput.value) || 200;
      delayTime = (parseInt(delayInput.value) || 2) * 1000;
      onlyNotFollowingBack = !!onlyNotBackInput.checked;
      running = true;
      paused = false;
      updateButtonStates('start');
      logMessage('▶ 已开始任务');
      updateProgress();
      autoUnfollow();
    });

    pauseBtn.addEventListener('click', (e) => {
      if (!running) return;
      paused = !paused;
      updateButtonStates(paused ? 'pause' : 'resume');
      logMessage(paused ? '⏸ 已暂停' : '▶ 已继续');
    });

    stopBtn.addEventListener('click', () => {
      if (!running) { logMessage('脚本未在运行'); return; }
      running = false;
      paused = false;
      updateButtonStates('stop');
      logMessage('⏹ 已停止');
    });

    // 初始按钮状态
    updateButtonStates('idle');
  }

  // ---------------- button visuals ----------------
  function updateButtonStates(state) {
    const startBtn = document.getElementById('startUnf');
    const pauseBtn = document.getElementById('pauseUnf');
    const stopBtn = document.getElementById('stopUnf');
    if (!startBtn || !pauseBtn || !stopBtn) return;

    if (state === 'start') {
      startBtn.disabled = true; startBtn.style.opacity = '0.6';
      pauseBtn.disabled = false; pauseBtn.style.opacity = '1'; pauseBtn.innerText = '⏸ 暂停';
      stopBtn.disabled = false; stopBtn.style.opacity = '1';
    } else if (state === 'pause') {
      pauseBtn.innerText = '▶ 继续';
      pauseBtn.style.opacity = '0.6';
    } else if (state === 'resume') {
      pauseBtn.innerText = '⏸ 暂停';
      pauseBtn.style.opacity = '1';
    } else if (state === 'stop') {
      startBtn.disabled = false; startBtn.style.opacity = '1';
      pauseBtn.disabled = true; pauseBtn.style.opacity = '0.6'; pauseBtn.innerText = '⏸ 暂停';
      stopBtn.disabled = true; stopBtn.style.opacity = '0.6';
    } else { // idle
      startBtn.disabled = false; startBtn.style.opacity = '1';
      pauseBtn.disabled = true; pauseBtn.style.opacity = '0.6'; pauseBtn.innerText = '⏸ 暂停';
      stopBtn.disabled = true; stopBtn.style.opacity = '0.6';
    }
  }

  // ---------------- logging & progress ----------------
  function logMessage(msg) {
    const box = document.getElementById('logBox');
    if (!box) return console.log(msg);
    const line = document.createElement('div');
    line.textContent = `${now()} - ${msg}`;
    box.appendChild(line);
    box.scrollTop = box.scrollHeight;
  }

  function updateProgress() {
    const pText = document.getElementById('progressText');
    const pBar = document.getElementById('progressBar');
    if (!pText || !pBar) return;
    pText.textContent = `${unfollowCount} / ${maxUnfollows}`;
    const pct = Math.min(100, (maxUnfollows > 0 ? (unfollowCount / maxUnfollows) * 100 : 0));
    pBar.style.width = `${pct}%`;
    if (pct >= 100) pBar.style.background = 'linear-gradient(90deg,#10b981,#059669)';
  }

  // ---------------- init ----------------
  try {
    window.addEventListener('load', () => setTimeout(createPanel, 900));
    // also create panel earlier if DOM already ready
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
      setTimeout(createPanel, 300);
    }
  } catch (e) {
    console.error('初始化面板失败', e);
  }

})();