Greasy Fork

Greasy Fork is available in English.

X 用户队列助手(自动化增强版 v2.2)

粘贴导入 -> 自动跳转 -> 自动点击关注 -> 随机延迟。已关注用户急速跳转,未关注用户安全延迟。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @license MIT
// @name         X 用户队列助手(自动化增强版 v2.2)
// @namespace    local
// @version      2.2
// @description  粘贴导入 -> 自动跳转 -> 自动点击关注 -> 随机延迟。已关注用户急速跳转,未关注用户安全延迟。
// @match        https://x.com/*
// @match        https://twitter.com/*
// @run-at       document-end
// @grant        none
// ==/UserScript==

(() => {
  "use strict";

  const ROOT_ID = "x_queue_helper_root_v4_auto";
  if (document.getElementById(ROOT_ID)) return;

  const STORAGE_KEY = "x_queue_helper_state_v2"; 

  const RESERVED = new Set([
    "", "home", "explore", "notifications", "messages", "i", "settings",
    "compose", "search", "login", "logout", "signup", "intent", "hashtag",
    "tos", "privacy"
  ]);

  const FOLLOW_TEXTS = ["\u5173\u6CE8", "Follow"];
  const FOLLOWING_TEXTS = ["\u5DF2\u5173\u6CE8", "Following"];

  const NOTFOUND_TEXTS = [
    "\u6B64\u8D26\u53F7\u4E0D\u5B58\u5728",
    "\u8FD9\u4E2A\u8D26\u53F7\u4E0D\u5B58\u5728",
    "This account doesn't exist",
    "This account doesn\u2019t exist",
  ];

  const SUSPENDED_TEXTS = ["\u8D26\u53F7\u5DF2\u88AB\u51BB\u7ED3", "Account suspended"];

  const STATUS_LABEL = {
    pending: "\u5F85\u5904\u7406",
    follow_clicked: "\u2705 \u5DF2\u81EA\u52A8\u5173\u6CE8",
    following_detected: "\u2714 \u539F\u672C\u5DF2\u5173",
    not_found: "\u274C \u4E0D\u5B58\u5728",
    suspended: "\u26D4 \u51BB\u7ED3",
    skipped: "\u23ED \u8DF3\u8FC7",
  };

  // === 23 人名单 ===
  const PRESET_USERNAMES = [
    "VitalikButerin",
    "hwwonx",
    "tkstanczak",
    "brucexu_eth",
    "nixorokish",
    "JBSchweitzer",
    "TimBeiko",
    "austingriffith",
    "DavideCrapis",
    "KhanAbbas201",
    "lanhubiji",
    "tmel0211",
    "nake13",
    "sassal0x",
    "haydenzadams",
    "superphiz",
    "owocki",
    "dankrad",
    "protolambda",
    "terencechain",
    "jessepollak",
    "chaowxyz",
    "jasoncrawford"
  ];

  const now = () => Date.now();
  // 随机延迟工具:min 到 max 毫秒
  const randomDelay = (min, max) => Math.floor(Math.random() * (max - min + 1) + min);
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

  function loadState() {
    try {
      return JSON.parse(localStorage.getItem(STORAGE_KEY) || "null") || {
        queue: [],
        idx: 0,
        status: {},
        open: false,
        autoMode: false 
      };
    } catch {
      return { queue: [], idx: 0, status: {}, open: false, autoMode: false };
    }
  }

  function saveState(state) {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
  }

  function normalizeText(text) {
    return (text || "").replace(/\uFF20/g, "@").replace(/\u3000/g, " ").replace(/\r/g, "\n");
  }

  function parseUsernames(rawText) {
    const text = normalizeText(rawText);
    const out = new Set();
    const urlRe = /https?:\/\/(?:www\.)?(?:x\.com|twitter\.com)\/([A-Za-z0-9_]{1,15})/gi;
    for (let m; (m = urlRe.exec(text));) out.add(m[1]);
    const atRe = /@([A-Za-z0-9_]{1,15})/g;
    for (let m; (m = atRe.exec(text));) out.add(m[1]);
    const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
    for (const line0 of lines) {
      if (line0.includes("http") || line0.includes("@")) continue;
      if (/^[A-Za-z0-9_]{1,15}$/.test(line0)) out.add(line0);
    }
    return [...out];
  }

  function includesAny(haystack, words) {
    if (!haystack) return false;
    return words.some((w) => haystack === w || haystack.includes(w));
  }

  function getUsernameFromUrl() {
    const seg = (location.pathname || "/").split("/").filter(Boolean)[0] || "";
    if (RESERVED.has(seg)) return null;
    return seg;
  }

  function gotoUser(username) {
    if (!username) return;
    location.href = `https://x.com/${encodeURIComponent(username)}`;
  }

  function setStatus(state, username, status, meta) {
    if (!username) return;
    state.status[username] = { status, updatedAt: now(), ...(meta || {}) };
    saveState(state);
    render(); 
  }

  function pageTextIncludesAny(list) {
    const t = (document.body && document.body.innerText ? document.body.innerText : "").slice(0, 25000);
    return list.some((s) => t.includes(s));
  }

  // 获取按钮状态
  function getFollowButtonState() {
    const buttons = Array.from(document.querySelectorAll("button"));
    const norm = (s) => (s || "").trim();
    for (const b of buttons) {
      const txt = norm(b.innerText);
      const aria = norm(b.getAttribute("aria-label"));
      const combined = `${txt} ${aria}`.trim();
      if (includesAny(combined, FOLLOWING_TEXTS)) return { button: b, state: "following", combined };
      if (includesAny(combined, FOLLOW_TEXTS)) return { button: b, state: "follow", combined };
    }
    return null;
  }

  function detectAndMarkFromProfile(state) {
    const u = getUsernameFromUrl();
    if (!u) return null;

    if (pageTextIncludesAny(NOTFOUND_TEXTS)) {
      setStatus(state, u, "not_found");
      return "not_found";
    }
    if (pageTextIncludesAny(SUSPENDED_TEXTS)) {
      setStatus(state, u, "suspended");
      return "suspended";
    }

    const info = getFollowButtonState();
    if (!info) return "unknown";

    if (info.state === "following") {
      setStatus(state, u, "following_detected");
      return "following";
    }
    if (info.state === "follow") {
      return "can_follow";
    }
    return "unknown";
  }

  function nextPendingIndex(state, fromIdx) {
    if (!state.queue.length) return 0;
    for (let i = fromIdx; i < state.queue.length; i++) {
      const u = state.queue[i];
      const s = state.status[u] && state.status[u].status;
      if (!s || s === "pending") return i;
    }
    return -1; 
  }

  // ================= 自动化核心逻辑 =================
  async function runAutoLoop() {
    const st = loadState();
    if (!st.autoMode) return;
    
    const currentQueueUser = st.queue[st.idx];
    const urlUser = getUsernameFromUrl();

    if (!currentQueueUser) {
        alert("队列已结束或为空,自动停止");
        st.autoMode = false;
        saveState(st);
        render();
        return;
    }

    if (urlUser && urlUser.toLowerCase() !== currentQueueUser.toLowerCase()) {
       console.log(`[Auto] URL用户(${urlUser})与队列用户(${currentQueueUser})不一致,准备跳转...`);
       await sleep(1000);
       gotoUser(currentQueueUser);
       return;
    }

    logToUI(`[Auto] 正在分析: ${currentQueueUser}...`);
    
    // 页面加载等待 (X需要时间渲染DOM,不能太短)
    const waitLoadTime = randomDelay(2000, 4000);
    await sleep(waitLoadTime);

    const freshState = loadState();
    if (!freshState.autoMode) return;

    const pageStatus = detectAndMarkFromProfile(freshState);
    logToUI(`[Auto] 状态: ${pageStatus}`);

    // === 优化分支:已关注急速跳转 ===
    if (pageStatus === "following") {
        const quickDelay = randomDelay(500, 800); // 极短延迟
        logToUI(`[Auto] 已关注,${quickDelay}ms 后急速跳转...`);
        await sleep(quickDelay);
        goToNextUserAndSave();
        return;
    }

    // === 异常分支:不存在或冻结 ===
    if (pageStatus === "not_found" || pageStatus === "suspended") {
        logToUI(`[Auto] 账号异常,稍后跳转...`);
        await sleep(randomDelay(1000, 2000));
        goToNextUserAndSave();
        return;
    }

    // === 操作分支:执行关注 ===
    if (pageStatus === "can_follow") {
        const info = getFollowButtonState();
        if (info && info.button) {
            logToUI(`[Auto] 点击关注...`);
            info.button.click();
            
            setStatus(freshState, currentQueueUser, "follow_clicked");
            
            // 点击后必须长冷却,模拟真人浏览
            const cooldown = randomDelay(3000, 6000);
            logToUI(`[Auto] 已点击,冷却 ${cooldown}ms ...`);
            await sleep(cooldown);
            
            goToNextUserAndSave();
        } else {
            logToUI(`[Auto] 异常:找不到按钮DOM`);
            await sleep(2000);
            goToNextUserAndSave();
        }
        return;
    }

    logToUI(`[Auto] 状态未知,可能加载慢,跳过...`);
    await sleep(2000);
    goToNextUserAndSave();
  }

  function goToNextUserAndSave() {
      const st = loadState();
      if (!st.autoMode) return;
      
      const nextIdx = nextPendingIndex(st, st.idx + 1);
      if (nextIdx === -1) {
          st.autoMode = false;
          saveState(st);
          render();
          alert("自动化队列已全部完成!");
          return;
      }
      
      st.idx = nextIdx;
      saveState(st);
      render();
      
      const nextUser = st.queue[nextIdx];
      logToUI(`[Auto] -> ${nextUser}`);
      gotoUser(nextUser);
  }

  // ================= UI 部分 =================
  const state = loadState();

  function mount() {
    if (document.getElementById(ROOT_ID)) return;
    const root = document.createElement("div");
    root.id = ROOT_ID;
    root.style.cssText = "all:initial; position:fixed; right:12px; bottom:20vh; z-index:2147483647;";

    const fab = document.createElement("button");
    fab.textContent = state.autoMode ? "Auto运行中..." : "助手控制台";
    fab.style.cssText = `all:initial; display:inline-block; background:${state.autoMode ? '#00ba7c' : '#1d9bf0'}; color:#fff; border-radius:999px; padding:10px 12px; font:13px system-ui; box-shadow:0 5px 15px rgba(0,0,0,.3); cursor:pointer;`;

    const panel = document.createElement("div");
    panel.style.cssText = `display:${state.open ? "block" : "none"}; margin-top:10px; width:360px; background:#000; color:#eee; border:1px solid #333; border-radius:12px; padding:12px; font:12px system-ui;`;

    const ta = document.createElement("textarea");
    ta.placeholder = "在此粘贴用户名列表...";
    ta.style.cssText = "display:block; width:100%; height:60px; background:#111; color:#fff; border:1px solid #333; margin-bottom:8px;";

    const logBox = document.createElement("div");
    logBox.style.cssText = "color:#0f0; margin-bottom:8px; height:20px; overflow:hidden; white-space:nowrap; text-overflow:ellipsis;";

    const btnContainer = document.createElement("div");
    btnContainer.style.cssText = "display:flex; gap:5px; flex-wrap:wrap;";

    const mkBtn = (txt, color, cb) => {
      const b = document.createElement("button");
      b.textContent = txt;
      b.style.cssText = `background:${color}; color:#fff; border:none; padding:5px 10px; border-radius:4px; cursor:pointer;`;
      b.onclick = cb;
      return b;
    };

    btnContainer.appendChild(mkBtn("导入", "#555", () => {
       const list = parseUsernames(ta.value);
       if(!list.length) return alert("未识别到用户名");
       state.queue = list;
       state.idx = 0;
       saveState(state);
       render();
       alert(`已导入 ${list.length} 人`);
    }));

    const btnAutoStart = mkBtn("▶ 启动自动关注", "#00ba7c", () => {
        if(!state.queue.length) return alert("请先导入");
        if(confirm("警告:自动化操作有封号风险。\n\n已优化:遇到已关注用户会快速跳过。\n遇到未关注用户会执行安全延迟。\n\n确定开始吗?")) {
            state.autoMode = true;
            saveState(state);
            render();
            runAutoLoop();
        }
    });

    const btnAutoStop = mkBtn("⏹ 停止", "#f4212e", () => {
        state.autoMode = false;
        saveState(state);
        render();
        logToUI("已手动停止自动化");
    });

    const btnReset = mkBtn("清空队列", "#333", () => {
        state.queue = [];
        state.idx = 0;
        state.status = {};
        state.autoMode = false;
        saveState(state);
        render();
    });
    
    // 载入预设
    const btnPreset = mkBtn("载入预设(Alt+D)", "#1d9bf0", () => {
        ta.value = PRESET_USERNAMES.join("\n");
        const list = parseUsernames(ta.value);
        state.queue = list;
        state.idx = 0;
        saveState(state);
        render();
    });

    btnContainer.appendChild(btnAutoStart);
    btnContainer.appendChild(btnAutoStop);
    btnContainer.appendChild(btnPreset);
    btnContainer.appendChild(btnReset);

    const statusDiv = document.createElement("div");
    statusDiv.style.cssText = "margin-top:10px; border-top:1px solid #333; padding-top:5px; max-height:150px; overflow:auto;";

    panel.appendChild(ta);
    panel.appendChild(logBox);
    panel.appendChild(btnContainer);
    panel.appendChild(statusDiv);
    root.appendChild(fab);
    root.appendChild(panel);
    document.body.appendChild(root);

    fab.onclick = () => {
      state.open = !state.open;
      saveState(state);
      panel.style.display = state.open ? "block" : "none";
    };

    mount.refs = { statusDiv, logBox, fab, btnAutoStart, btnAutoStop };
  }

  function logToUI(msg) {
      if(mount.refs && mount.refs.logBox) mount.refs.logBox.textContent = msg;
      console.log(msg);
  }

  function render() {
    if (!mount.refs) return;
    const { statusDiv, fab, btnAutoStart, btnAutoStop } = mount.refs;
    
    fab.textContent = state.autoMode ? "Auto运行中..." : "助手控制台";
    fab.style.background = state.autoMode ? '#00ba7c' : '#1d9bf0';
    btnAutoStart.style.display = state.autoMode ? 'none' : 'inline-block';
    btnAutoStop.style.display = state.autoMode ? 'inline-block' : 'none';

    statusDiv.innerHTML = "";
    if (!state.queue.length) {
        statusDiv.textContent = "队列为空";
        return;
    }

    state.queue.forEach((u, i) => {
        const s = (state.status[u] && state.status[u].status) || "pending";
        const row = document.createElement("div");
        row.style.cssText = `padding:2px; font-size:11px; ${i === state.idx ? "background:#222; color:#fff;" : "color:#888;"}`;
        let label = STATUS_LABEL[s] || "待处理";
        if(i === state.idx && state.autoMode) label += " <ING>";
        row.textContent = `${i+1}. ${u} - ${label}`;
        statusDiv.appendChild(row);
    });
  }

  function init() {
      mount();
      render();
      if (state.autoMode) {
          logToUI("检测到自动模式开启,3秒后继续...");
          setTimeout(runAutoLoop, 3000);
      }
  }
  
  window.addEventListener("keydown", (e) => {
      if (e.altKey && e.key.toLowerCase() === "d") {
          mount.refs && mount.refs.ta ? (mount.refs.ta.value = PRESET_USERNAMES.join("\n")) : null;
      }
  });

  init();
})();