取关脚本

x

(async () => {
  const sleep = (ms) => new Promise(r => setTimeout(r, ms));

  let unfollowed = 0;
  let rounds = 0;
  const maxUnfollow = 100; // 一次最多取关多少,自己改小一点更安全

  function getButtons() {
    const buttons = [...document.querySelectorAll('button')];
    return buttons.filter(btn => {
      const text = btn.innerText.trim();
      return ['Following', '正在关注', '已关注'].includes(text);
    });
  }

  while (unfollowed < maxUnfollow && rounds < 50) {
    const buttons = getButtons();

    for (const btn of buttons) {
      if (unfollowed >= maxUnfollow) break;

      btn.scrollIntoView({ behavior: 'smooth', block: 'center' });
      await sleep(1500);
      btn.click();
      await sleep(1000);

      const confirmBtn = [...document.querySelectorAll('button')].find(b => {
        const t = b.innerText.trim();
        return ['Unfollow', '取消关注'].includes(t);
      });

      if (confirmBtn) {
        confirmBtn.click();
        unfollowed++;
        console.log(`已取关: ${unfollowed}`);
        await sleep(3000 + Math.random() * 4000); // 随机慢一点
      }
    }

    window.scrollBy(0, 1200);
    await sleep(3000);
    rounds++;
  }

  console.log(`完成,本次共取关 ${unfollowed} 个`);
})();

youtube

(async () => {
  const wait = ms => new Promise(r => setTimeout(r, ms));
  const log = (...args) => console.log("[YT-UNSUB]", ...args);

  const TEXT_SUBBED = ["已订阅", "已訂閱", "subscribed"];
  const TEXT_UNSUB = ["退订", "退訂", "取消订阅", "取消訂閱", "unsubscribe"];
  const TEXT_CONFIRM = ["退订", "退訂", "取消订阅", "取消訂閱", "unsubscribe"];

  function norm(s) {
    return (s || "").replace(/\s+/g, " ").trim().toLowerCase();
  }

  function visible(el) {
    if (!el) return false;
    const r = el.getBoundingClientRect();
    const st = getComputedStyle(el);
    return r.width > 0 && r.height > 0 && st.display !== "none" && st.visibility !== "hidden";
  }

  function hasAny(text, arr) {
    const t = norm(text);
    return arr.some(x => t.includes(x.toLowerCase()));
  }

  function getRows() {
    return [...document.querySelectorAll("ytd-channel-renderer")];
  }

  function getName(row) {
    return row.querySelector("#main-link")?.textContent?.trim() || "未知频道";
  }

  function getSubscribedBtn(row) {
    return [...row.querySelectorAll("button, tp-yt-paper-button, yt-button-shape button")]
      .find(btn => visible(btn) && hasAny(btn.innerText || btn.textContent || "", TEXT_SUBBED));
  }

  function getVisibleActionEls() {
    return [...document.querySelectorAll(`
      tp-yt-paper-item,
      ytd-menu-service-item-renderer,
      button,
      tp-yt-paper-button,
      div[role="menuitem"],
      div[role="button"],
      yt-formatted-string
    `)].filter(visible);
  }

  function findUnsubEntry() {
    const els = getVisibleActionEls();
    return els.find(el => {
      const txt = norm(el.innerText || el.textContent || "");
      const aria = norm(el.getAttribute("aria-label") || "");
      return TEXT_UNSUB.some(k => txt.includes(k.toLowerCase()) || aria.includes(k.toLowerCase()));
    });
  }

  function findConfirmBtn() {
    const els = [...document.querySelectorAll(`
      tp-yt-paper-dialog button,
      tp-yt-paper-dialog tp-yt-paper-button,
      ytd-popup-container button,
      ytd-popup-container tp-yt-paper-button,
      button,
      tp-yt-paper-button
    `)].filter(visible);

    return els.find(el => {
      const txt = norm(el.innerText || el.textContent || "");
      const aria = norm(el.getAttribute("aria-label") || "");
      return TEXT_CONFIRM.some(k => txt.includes(k.toLowerCase()) || aria.includes(k.toLowerCase()));
    });
  }

  async function pressEsc() {
    document.dispatchEvent(new KeyboardEvent("keydown", {
      key: "Escape",
      code: "Escape",
      keyCode: 27,
      which: 27,
      bubbles: true
    }));
    await wait(500);
  }

  async function autoScrollLoad(rounds = 25) {
    let lastHeight = 0;
    for (let i = 0; i < rounds; i++) {
      window.scrollTo(0, document.documentElement.scrollHeight);
      await wait(1500);
      const h = document.documentElement.scrollHeight;
      log(`滚动加载 ${i + 1}/${rounds}`);
      if (h === lastHeight) break;
      lastHeight = h;
    }
    window.scrollTo(0, 0);
    await wait(1000);
  }

  async function handleRow(row) {
    const name = getName(row);
    const btn = getSubscribedBtn(row);

    if (!btn) {
      log("跳过,没找到已订阅按钮:", name);
      return false;
    }

    row.scrollIntoView({ behavior: "smooth", block: "center" });
    await wait(700);

    log("正在处理:", name);
    btn.click();
    await wait(1200);

    let unsub = findUnsubEntry();
    if (!unsub) {
      await wait(1200);
      unsub = findUnsubEntry();
    }

    if (!unsub) {
      log("没找到“退订”入口:", name);
      await pressEsc();
      return false;
    }

    unsub.click();
    await wait(1200);

    let confirm = findConfirmBtn();
    if (!confirm) {
      await wait(1000);
      confirm = findConfirmBtn();
    }

    if (confirm) {
      confirm.click();
      log("已退订:", name);
      await wait(1800);
      return true;
    }

    log("未找到确认按钮,可能已直接退订:", name);
    await wait(1200);
    return true;
  }

  async function run(limit = 30) {
    await autoScrollLoad();

    let done = 0;
    const rows = getRows();

    for (const row of rows) {
      if (done >= limit) break;

      const ok = await handleRow(row);
      if (ok) done++;

      await wait(1000);
    }

    log(`本轮结束,共退订 ${done} 个频道`);
  }

  await run(30); // 可改成 10、20、50
})();

pixiv

(async () => {
  const STORAGE_KEY = '__pixiv_auto_unfollow__';

  const sleep = (ms) => new Promise(r => setTimeout(r, ms));

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

  function loadState() {
    try {
      return JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null');
    } catch {
      return null;
    }
  }

  function clearState() {
    localStorage.removeItem(STORAGE_KEY);
  }

  function isVisible(el) {
    if (!el) return false;
    const rect = el.getBoundingClientRect();
    return rect.width > 0 && rect.height > 0;
  }

  function getFollowButtons() {
    return [...document.querySelectorAll('button.charcoal-button')]
      .filter(el => {
        const text = (el.innerText || el.textContent || '').trim();
        return text === '已关注' && !el.disabled && isVisible(el);
      });
  }

  function getAllFollowButtons() {
    return [...document.querySelectorAll('button.charcoal-button')]
      .filter(el => {
        const text = (el.innerText || el.textContent || '').trim();
        return text === '已关注' && !el.disabled;
      });
  }

  function clickLikeHuman(el) {
    ['mouseover', 'mousedown', 'mouseup', 'click'].forEach(type => {
      el.dispatchEvent(new MouseEvent(type, {
        bubbles: true,
        cancelable: true,
        view: window
      }));
    });
  }

  function findConfirmButton() {
    const texts = ['取消关注', '解除关注', '确认', '確認', 'OK', 'Unfollow', 'フォロー解除'];
    return [...document.querySelectorAll('button, a, [role="button"], div')]
      .find(el => texts.includes((el.innerText || el.textContent || '').trim()));
  }

  async function unfollowOne(btn) {
    const beforeText = (btn.innerText || btn.textContent || '').trim();
    if (beforeText !== '已关注') return false;

    const beforeCount = getAllFollowButtons().length;

    btn.scrollIntoView({ behavior: 'smooth', block: 'center' });
    await sleep(700);

    try {
      btn.click();
    } catch {
      clickLikeHuman(btn);
    }

    await sleep(1200);

    let confirmBtn = findConfirmButton();
    if (confirmBtn) {
      try {
        confirmBtn.click();
      } catch {
        clickLikeHuman(confirmBtn);
      }
      await sleep(1800);
    }

    let afterCount = getAllFollowButtons().length;
    if (afterCount < beforeCount) return true;

    clickLikeHuman(btn);
    await sleep(1200);

    confirmBtn = findConfirmButton();
    if (confirmBtn) {
      try {
        confirmBtn.click();
      } catch {
        clickLikeHuman(confirmBtn);
      }
      await sleep(1800);
    }

    afterCount = getAllFollowButtons().length;
    return afterCount < beforeCount;
  }

  function getCurrentPageNumber() {
    const current = document.querySelector('span.charcoal-pagination-button[aria-current="page"]');
    if (!current) return null;
    const text = (current.innerText || current.textContent || '').trim();
    return /^\d+$/.test(text) ? parseInt(text, 10) : null;
  }

  function getNextPageLink() {
    const currentNum = getCurrentPageNumber();
    if (!currentNum) return null;

    return [...document.querySelectorAll('a.charcoal-pagination-button')]
      .find(el => {
        const text = (el.innerText || el.textContent || '').trim();
        return /^\d+$/.test(text) && parseInt(text, 10) === currentNum + 1;
      });
  }

  async function run() {
    let state = loadState();

    if (!state || !state.running) {
      state = {
        running: true,
        total: 0,
        startedAt: new Date().toISOString()
      };
      saveState(state);
    }

    const currentPage = getCurrentPageNumber() || '?';
    console.log(`开始处理第 ${currentPage} 页,累计已取消 ${state.total} 个`);

    let failStreak = 0;

    while (true) {
      const buttons = getFollowButtons();

      if (!buttons.length) {
        console.log(`第 ${currentPage} 页没有“已关注”按钮了`);
        break;
      }

      const ok = await unfollowOne(buttons[0]);

      if (ok) {
        state.total++;
        failStreak = 0;
        saveState(state);
        console.log(`累计已取消 ${state.total} 个`);
      } else {
        failStreak++;
        console.warn(`取消失败 ${failStreak} 次`);
        if (failStreak >= 3) break;
      }

      await sleep(1200 + Math.random() * 1000);
    }

    const nextLink = getNextPageLink();
    if (!nextLink) {
      console.log(`全部完成,总共取消 ${state.total} 个`);
      clearState();
      return;
    }

    console.log(`跳转到下一页:${nextLink.href}`);
    await sleep(1500);
    location.href = nextLink.href;
  }

  window.__pixivAutoUnfollowRun = run;
  window.__pixivAutoUnfollowStop = () => {
    clearState();
    console.log('已停止自动取消关注');
  };

  await run();
})();

pixiv 油猴版

// ==UserScript==
// @name         Pixiv 自动取消关注(Following 分页版)
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  在 Pixiv following 页面自动取消关注,并自动翻页继续执行
// @match        https://www.pixiv.net/users/*/following*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  const STORAGE_KEY = '__pixiv_auto_unfollow_state__';

  const CONFIG = {
    startDelay: 2500,
    betweenClicks: 1200,
    betweenPages: 2500,
    modalWait: 1400,
    confirmWait: 1800,
    failLimit: 3,
    jitter: 1000
  };

  const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

  function rand(base) {
    return base + Math.floor(Math.random() * CONFIG.jitter);
  }

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

  function loadState() {
    try {
      return JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null');
    } catch (e) {
      return null;
    }
  }

  function clearState() {
    localStorage.removeItem(STORAGE_KEY);
  }

  function getState() {
    let state = loadState();
    if (!state) {
      state = {
        running: true,
        total: 0,
        pageDone: 0,
        startedAt: new Date().toISOString(),
        lastPage: null
      };
      saveState(state);
    }
    return state;
  }

  function setState(patch) {
    const next = { ...getState(), ...patch };
    saveState(next);
    renderPanel(next);
    return next;
  }

  function isVisible(el) {
    if (!el) return false;
    const rect = el.getBoundingClientRect();
    const style = window.getComputedStyle(el);
    return (
      rect.width > 0 &&
      rect.height > 0 &&
      style.visibility !== 'hidden' &&
      style.display !== 'none'
    );
  }

  function textOf(el) {
    return (el?.innerText || el?.textContent || '').trim();
  }

  function clickLikeHuman(el) {
    ['mouseover', 'mousedown', 'mouseup', 'click'].forEach(type => {
      el.dispatchEvent(new MouseEvent(type, {
        bubbles: true,
        cancelable: true,
        view: window
      }));
    });
  }

  function getCurrentPageNumber() {
    const current = document.querySelector('span.charcoal-pagination-button[aria-current="page"]');
    if (!current) return null;
    const text = textOf(current);
    return /^\d+$/.test(text) ? parseInt(text, 10) : null;
  }

  function getNextPageLink() {
    const currentNum = getCurrentPageNumber();
    if (!currentNum) return null;

    return [...document.querySelectorAll('a.charcoal-pagination-button')]
      .find(el => {
        const text = textOf(el);
        return /^\d+$/.test(text) && parseInt(text, 10) === currentNum + 1;
      });
  }

  function getFollowButtons() {
    return [...document.querySelectorAll('button.charcoal-button')]
      .filter(el => textOf(el) === '已关注' && !el.disabled && isVisible(el));
  }

  function getAllFollowButtons() {
    return [...document.querySelectorAll('button.charcoal-button')]
      .filter(el => textOf(el) === '已关注' && !el.disabled);
  }

  function findConfirmButton() {
    const texts = ['取消关注', '解除关注', '确认', '確認', 'OK', 'Unfollow', 'フォロー解除'];
    return [...document.querySelectorAll('button, a, [role="button"], div')]
      .find(el => texts.includes(textOf(el)) && isVisible(el));
  }

  async function waitForPageReady() {
    for (let i = 0; i < 30; i++) {
      if (document.querySelector('button.charcoal-button') || document.querySelector('.charcoal-pagination-button')) {
        return true;
      }
      await sleep(500);
    }
    return false;
  }

  async function unfollowOne(btn) {
    const beforeCount = getAllFollowButtons().length;

    btn.scrollIntoView({ behavior: 'smooth', block: 'center' });
    await sleep(700);

    try {
      btn.click();
    } catch (e) {
      clickLikeHuman(btn);
    }

    await sleep(CONFIG.modalWait);

    let confirmBtn = findConfirmButton();
    if (confirmBtn) {
      try {
        confirmBtn.click();
      } catch (e) {
        clickLikeHuman(confirmBtn);
      }
      await sleep(CONFIG.confirmWait);
    }

    let afterCount = getAllFollowButtons().length;
    if (afterCount < beforeCount) return true;

    try {
      clickLikeHuman(btn);
    } catch (e) {}

    await sleep(CONFIG.modalWait);

    confirmBtn = findConfirmButton();
    if (confirmBtn) {
      try {
        confirmBtn.click();
      } catch (e) {
        clickLikeHuman(confirmBtn);
      }
      await sleep(CONFIG.confirmWait);
    }

    afterCount = getAllFollowButtons().length;
    return afterCount < beforeCount;
  }

  function createPanel() {
    if (document.getElementById('__pixiv_unfollow_panel__')) return;

    const panel = document.createElement('div');
    panel.id = '__pixiv_unfollow_panel__';
    panel.style.cssText = `
      position: fixed;
      top: 16px;
      right: 16px;
      z-index: 999999;
      width: 260px;
      background: rgba(20,20,20,0.92);
      color: #fff;
      border-radius: 12px;
      padding: 12px;
      box-shadow: 0 8px 24px rgba(0,0,0,.3);
      font-size: 13px;
      line-height: 1.5;
      font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
    `;

    panel.innerHTML = `
      <div style="font-weight:700;margin-bottom:8px;">Pixiv 自动取消关注</div>
      <div id="__pixiv_unfollow_status__">初始化中...</div>
      <div style="margin-top:10px;display:flex;gap:8px;">
        <button id="__pixiv_unfollow_stop__" style="flex:1;padding:6px 8px;border:none;border-radius:8px;background:#ff5f57;color:#fff;cursor:pointer;">停止</button>
        <button id="__pixiv_unfollow_hide__" style="flex:1;padding:6px 8px;border:none;border-radius:8px;background:#444;color:#fff;cursor:pointer;">隐藏</button>
      </div>
    `;

    document.body.appendChild(panel);

    document.getElementById('__pixiv_unfollow_stop__').onclick = () => {
      clearState();
      alert('已停止自动取消关注');
    };

    document.getElementById('__pixiv_unfollow_hide__').onclick = () => {
      panel.style.display = 'none';
    };
  }

  function renderPanel(state) {
    createPanel();
    const el = document.getElementById('__pixiv_unfollow_status__');
    if (!el) return;

    const page = getCurrentPageNumber();
    el.innerHTML = `
      <div>状态:${state.running ? '运行中' : '已停止'}</div>
      <div>当前页:${page ?? '?'}</div>
      <div>总计取消:${state.total}</div>
      <div>本页取消:${state.pageDone}</div>
    `;
  }

  async function main() {
    const state = getState();
    if (!state.running) return;

    createPanel();
    renderPanel(state);

    const ready = await waitForPageReady();
    if (!ready) {
      console.warn('页面元素未准备好,停止运行');
      return;
    }

    await sleep(CONFIG.startDelay);

    let currentPage = getCurrentPageNumber();
    if (!currentPage) {
      console.warn('未识别到当前页码');
    }

    setState({
      pageDone: 0,
      lastPage: currentPage
    });

    console.log(`开始处理第 ${currentPage} 页`);

    let failStreak = 0;

    while (true) {
      const currentState = loadState();
      if (!currentState || !currentState.running) {
        console.log('检测到停止指令,退出');
        return;
      }

      const buttons = getFollowButtons();

      if (!buttons.length) {
        console.log(`第 ${currentPage} 页没有更多“已关注”按钮`);
        break;
      }

      const ok = await unfollowOne(buttons[0]);

      if (ok) {
        const s = getState();
        setState({
          total: s.total + 1,
          pageDone: s.pageDone + 1
        });
        failStreak = 0;
      } else {
        failStreak++;
        console.warn(`取消失败 ${failStreak} 次`);
        if (failStreak >= CONFIG.failLimit) {
          console.warn('连续失败过多,结束本页');
          break;
        }
      }

      await sleep(rand(CONFIG.betweenClicks));
    }

    const nextLink = getNextPageLink();

    if (!nextLink) {
      console.log('没有下一页,任务结束');
      const finalState = getState();
      setState({ running: false });
      alert(`自动取消关注完成,共取消 ${finalState.total} 个`);
      clearState();
      return;
    }

    console.log(`跳转下一页:${nextLink.href}`);
    await sleep(CONFIG.betweenPages);
    location.href = nextLink.href;
  }

  main();
})();

bilibili 油猴版

// ==UserScript==
// @name         Bilibili 批量取关(接口版)
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  通过接口自动分页获取关注列表并批量取关,不依赖页面按钮点击
// @match        https://space.bilibili.com/*
// @match        https://www.bilibili.com/*
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const CONFIG = {
    pageSize: 50,             // 每页拉取多少关注
    minDelay: 2500,           // 每次取关最小间隔
    maxDelay: 5000,           // 每次取关最大间隔
    batchPauseEvery: 20,      // 每处理多少个暂停一次
    batchPauseTime: 30000,    // 暂停时长
    requestTimeout: 15000,    // 请求超时
    debug: true
  };

  const state = {
    running: false,
    stopped: false,
    totalFetched: 0,
    totalSuccess: 0,
    totalFailed: 0,
    currentPage: 1,
    users: []
  };

  const sleep = (ms) => new Promise(r => setTimeout(r, ms));
  const rand = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;

  function log(...args) {
    console.log('[BiliAPIUnfollow]', ...args);
    const el = document.querySelector('#bili-api-unfollow-status');
    if (el) el.textContent = args.join(' ');
  }

  function updateCounter() {
    const el = document.querySelector('#bili-api-unfollow-counter');
    if (el) {
      el.innerHTML = `
        已获取: ${state.totalFetched}<br>
        成功: ${state.totalSuccess}<br>
        失败: ${state.totalFailed}<br>
        当前页: ${state.currentPage}
      `;
    }
  }

  function getCookie(name) {
    const match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[$()*+.?[\\\]^{|}]/g, '\\$&') + '=([^;]*)'));
    return match ? decodeURIComponent(match[1]) : null;
  }

  function getCsrf() {
    return getCookie('bili_jct');
  }

  function getUid() {
    return getCookie('DedeUserID');
  }

  async function fetchWithTimeout(url, options = {}, timeout = CONFIG.requestTimeout) {
    const controller = new AbortController();
    const id = setTimeout(() => controller.abort(), timeout);

    try {
      const res = await fetch(url, {
        ...options,
        credentials: 'include',
        signal: controller.signal
      });
      clearTimeout(id);
      return res;
    } catch (err) {
      clearTimeout(id);
      throw err;
    }
  }

  async function getFollowings(page) {
    const uid = getUid();
    if (!uid) throw new Error('未获取到 DedeUserID,请确认已登录');

    const url = `https://api.bilibili.com/x/relation/followings?vmid=${uid}&pn=${page}&ps=${CONFIG.pageSize}&order=desc&jsonp=jsonp`;

    const res = await fetchWithTimeout(url, {
      method: 'GET'
    });

    const data = await res.json();

    if (data.code !== 0) {
      throw new Error(`获取关注列表失败: code=${data.code}, message=${data.message}`);
    }

    return data?.data?.list || [];
  }

  async function unfollowUser(mid) {
    const csrf = getCsrf();
    if (!csrf) throw new Error('未获取到 bili_jct(csrf),请确认已登录');

    const body = new URLSearchParams({
      fid: String(mid),
      act: '2',
      re_src: '11',
      csrf: csrf
    });

    const res = await fetchWithTimeout('https://api.bilibili.com/x/relation/modify', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
      },
      body: body.toString()
    });

    const data = await res.json();
    return data;
  }

  async function fetchAllUsers() {
    state.users = [];
    state.currentPage = 1;
    state.totalFetched = 0;
    updateCounter();

    while (state.running && !state.stopped) {
      log(`正在读取第 ${state.currentPage} 页关注列表...`);
      const list = await getFollowings(state.currentPage);

      if (!list.length) {
        log('关注列表读取完成');
        break;
      }

      state.users.push(...list);
      state.totalFetched = state.users.length;
      updateCounter();

      log(`第 ${state.currentPage} 页读取到 ${list.length} 个,累计 ${state.totalFetched} 个`);
      state.currentPage++;

      await sleep(1200);
    }
  }

  async function runUnfollow() {
    if (!state.users.length) {
      log('没有获取到任何关注用户');
      return;
    }

    log(`准备开始取关,共 ${state.users.length} 个`);

    for (let i = 0; i < state.users.length; i++) {
      if (!state.running || state.stopped) break;

      const user = state.users[i];
      const uname = user.uname || user.name || `mid:${user.mid}`;

      try {
        log(`正在取关 [${i + 1}/${state.users.length}] ${uname}`);

        const result = await unfollowUser(user.mid);

        if (result.code === 0) {
          state.totalSuccess++;
          log(`取关成功: ${uname}`);
        } else {
          state.totalFailed++;
          log(`取关失败: ${uname} code=${result.code} msg=${result.message || ''}`);
        }

      } catch (err) {
        state.totalFailed++;
        log(`取关异常: ${uname} ${err.message || err}`);
      }

      updateCounter();

      if (state.totalSuccess > 0 && state.totalSuccess % CONFIG.batchPauseEvery === 0) {
        log(`已成功取关 ${state.totalSuccess} 个,暂停 ${CONFIG.batchPauseTime / 1000} 秒`);
        await sleep(CONFIG.batchPauseTime);
      } else {
        await sleep(rand(CONFIG.minDelay, CONFIG.maxDelay));
      }
    }

    log(`任务结束。成功 ${state.totalSuccess},失败 ${state.totalFailed}`);
  }

  async function startTask() {
    if (state.running) {
      alert('任务已经在运行中');
      return;
    }

    const csrf = getCsrf();
    const uid = getUid();

    if (!csrf || !uid) {
      alert('未检测到登录态,请先登录 Bilibili');
      return;
    }

    state.running = true;
    state.stopped = false;
    state.totalFetched = 0;
    state.totalSuccess = 0;
    state.totalFailed = 0;
    state.currentPage = 1;
    state.users = [];
    updateCounter();

    try {
      await fetchAllUsers();

      if (!state.stopped) {
        await runUnfollow();
      }
    } catch (err) {
      console.error(err);
      log(`运行失败: ${err.message || err}`);
    }

    state.running = false;
    if (state.stopped) {
      log('任务已停止');
    }
  }

  function stopTask() {
    state.stopped = true;
    state.running = false;
    log('正在停止任务...');
  }

  function createPanel() {
    if (document.querySelector('#bili-api-unfollow-panel')) return;

    const panel = document.createElement('div');
    panel.id = 'bili-api-unfollow-panel';
    panel.style.cssText = `
      position: fixed;
      top: 80px;
      right: 20px;
      z-index: 999999;
      width: 270px;
      background: rgba(24,24,24,.95);
      color: #fff;
      border-radius: 10px;
      padding: 14px;
      box-shadow: 0 4px 16px rgba(0,0,0,.3);
      font-size: 14px;
      line-height: 1.5;
    `;

    panel.innerHTML = `
      <div style="font-size:16px;font-weight:bold;margin-bottom:8px;">B站批量取关(接口版)</div>
      <div id="bili-api-unfollow-counter" style="margin-bottom:8px;">
        已获取: 0<br>
        成功: 0<br>
        失败: 0<br>
        当前页: 1
      </div>
      <div id="bili-api-unfollow-status" style="margin-bottom:10px;color:#ddd;font-size:12px;">等待开始</div>
      <div style="display:flex;gap:8px;">
        <button id="bili-api-unfollow-start" style="flex:1;padding:8px;border:none;border-radius:6px;background:#00aeec;color:#fff;cursor:pointer;">开始</button>
        <button id="bili-api-unfollow-stop" style="flex:1;padding:8px;border:none;border-radius:6px;background:#fb7299;color:#fff;cursor:pointer;">停止</button>
      </div>
      <div style="margin-top:8px;font-size:12px;color:#bbb;">建议低速运行,避免风控</div>
    `;

    document.body.appendChild(panel);

    document.querySelector('#bili-api-unfollow-start').addEventListener('click', startTask);
    document.querySelector('#bili-api-unfollow-stop').addEventListener('click', stopTask);
  }

  window.addEventListener('load', () => {
    setTimeout(createPanel, 1500);
  });
})();

删除bilibili粉丝团勋章,有小问题但是能用

// ==UserScript==
// @name         B站粉丝勋章自动逐个删除-刷新/局部更新兼容版
// @namespace    https://tampermonkey.net/
// @version      2.2.0
// @description  自动逐个删除粉丝勋章,兼容页面刷新和局部更新两种模式
// @author       ChatGPT
// @match        *://link.bilibili.com/p/*
// @match        *://link.bilibili.com/*
// @match        *://live.bilibili.com/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const CONFIG = {
        debug: true,
        autoStart: true,
        pageReadyDelay: 3000,
        afterDeleteClickDelay: 1500,
        dialogAppearTimeout: 15000,
        afterConfirmDelay: 800,
        waitDeleteResultTimeout: 20000,
        resultCheckInterval: 800,
        maxDeleteCount: 500,

        runKey: 'bili_medal_auto_delete_running',
        stopKey: 'bili_medal_auto_delete_stop',
        countKey: 'bili_medal_auto_delete_count',
        lockKey: 'bili_medal_auto_delete_lock'
    };

    let pageUnloading = false;
    window.addEventListener('beforeunload', () => {
        pageUnloading = true;
    });

    function log(...args) {
        if (CONFIG.debug) console.log('[勋章自动删除]', ...args);
    }

    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    function text(el) {
        return (el?.innerText || el?.textContent || '').replace(/\s+/g, ' ').trim();
    }

    function isVisible(el) {
        if (!el) return false;
        const style = window.getComputedStyle(el);
        const rect = el.getBoundingClientRect();
        return style.display !== 'none' &&
            style.visibility !== 'hidden' &&
            rect.width > 0 &&
            rect.height > 0;
    }

    function fireMouse(el, type, x, y) {
        el.dispatchEvent(new MouseEvent(type, {
            view: window,
            bubbles: true,
            cancelable: true,
            clientX: x,
            clientY: y
        }));
    }

    function strongClick(el) {
        if (!el) return false;
        el.scrollIntoView({ behavior: 'smooth', block: 'center' });
        const rect = el.getBoundingClientRect();
        const x = rect.left + rect.width / 2;
        const y = rect.top + rect.height / 2;

        ['pointerover', 'mouseover', 'mouseenter', 'pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']
            .forEach(type => fireMouse(el, type, x, y));

        if (typeof el.click === 'function') el.click();
        return true;
    }

    function getRows() {
        return Array.from(document.querySelectorAll('tbody tr'))
            .filter(row => row.querySelector('.fans-medal-item'));
    }

    function getRowName(row) {
        if (!row) return '';
        const medal = row.querySelector('.fans-medal-content');
        const anchor = row.querySelector('a[title]');
        return `${text(medal)} / ${anchor?.getAttribute('title') || text(anchor) || ''}`.trim();
    }

    function getFirstRow() {
        const rows = getRows();
        return rows.length ? rows[0] : null;
    }

    function getFirstRowName() {
        return getRowName(getFirstRow());
    }

    function findDeleteBtnInRow(row) {
        if (!row) return null;
        const lastTd = row.querySelector('td:last-child');
        if (!lastTd) return null;
        const spans = Array.from(lastTd.querySelectorAll('span'));
        return spans.find(el => text(el) === '删除') || null;
    }

    function clickDeleteInRow(row) {
        const btn = findDeleteBtnInRow(row);
        if (!btn) return false;
        log('命中删除按钮:', btn.outerHTML);
        strongClick(btn);
        return true;
    }

    function getVisibleDialogs() {
        return Array.from(document.querySelectorAll('.link-popup-panel')).filter(isVisible);
    }

    function findNoticeDialog() {
        const dialogs = getVisibleDialogs();

        let dialog = dialogs.find(d => {
            const t = text(d);
            return t.includes('删除粉丝勋章须知') || t.includes('确认删除');
        });
        if (dialog) return dialog;

        dialog = dialogs.find(d => Array.from(d.querySelectorAll('button')).some(b => text(b).includes('确认删除')));
        if (dialog) return dialog;

        if (dialogs.length === 1) return dialogs[0];
        return null;
    }

    function findConfirmDeleteBtn(dialog) {
        if (!dialog) return null;
        const buttons = Array.from(dialog.querySelectorAll('button')).filter(isVisible);
        return buttons.find(btn => text(btn).includes('确认删除')) || null;
    }

    async function waitFor(fn, timeout = 5000, interval = 250) {
        const start = Date.now();
        while (Date.now() - start < timeout) {
            const result = fn();
            if (result) return result;
            await sleep(interval);
        }
        return null;
    }

    function forceEnableButton(btn) {
        if (!btn) return false;
        btn.disabled = false;
        btn.removeAttribute('disabled');
        btn.setAttribute('aria-disabled', 'false');
        btn.classList.remove('disabled');
        return true;
    }

    async function processNoticeDialog() {
        const dialog = await waitFor(() => findNoticeDialog(), CONFIG.dialogAppearTimeout, 300);
        if (!dialog) {
            log('失败:没找到须知弹窗');
            return false;
        }

        log('找到须知弹窗');

        const confirmBtn = findConfirmDeleteBtn(dialog);
        if (!confirmBtn) {
            log('失败:没找到确认删除按钮');
            return false;
        }

        forceEnableButton(confirmBtn);
        await sleep(300);
        strongClick(confirmBtn);
        await sleep(CONFIG.afterConfirmDelay);
        return true;
    }

    function setRunning(v) {
        localStorage.setItem(CONFIG.runKey, v ? '1' : '0');
    }

    function isRunning() {
        return localStorage.getItem(CONFIG.runKey) === '1';
    }

    function setStop(v) {
        localStorage.setItem(CONFIG.stopKey, v ? '1' : '0');
    }

    function isStopped() {
        return localStorage.getItem(CONFIG.stopKey) === '1';
    }

    function getDeleteCount() {
        return Number(localStorage.getItem(CONFIG.countKey) || '0');
    }

    function setDeleteCount(n) {
        localStorage.setItem(CONFIG.countKey, String(n));
    }

    function resetState() {
        setDeleteCount(0);
        setStop(false);
        localStorage.removeItem(CONFIG.lockKey);
    }

    function acquireLock() {
        if (localStorage.getItem(CONFIG.lockKey) === '1') return false;
        localStorage.setItem(CONFIG.lockKey, '1');
        return true;
    }

    function releaseLock() {
        localStorage.removeItem(CONFIG.lockKey);
    }

    async function waitTableReady(timeout = 20000) {
        const start = Date.now();
        while (Date.now() - start < timeout) {
            if (document.querySelector('tbody')) return true;
            await sleep(500);
        }
        return false;
    }

    async function waitDeleteResult(beforeName, beforeCount, timeout = CONFIG.waitDeleteResultTimeout) {
        const start = Date.now();

        while (Date.now() - start < timeout) {
            if (pageUnloading) {
                log('检测到页面即将刷新');
                return 'reload';
            }

            const rows = getRows();
            const currentCount = rows.length;
            const currentFirstName = rows.length ? getRowName(rows[0]) : '';

            log('等待删除结果中:', {
                beforeName,
                currentFirstName,
                beforeCount,
                currentCount
            });

            if (currentCount === 0) {
                log('列表已空');
                return 'empty';
            }

            if (currentCount < beforeCount) {
                log('检测到行数减少');
                return 'count_changed';
            }

            if (currentFirstName && currentFirstName !== beforeName) {
                log('检测到第一项已变化');
                return 'first_changed';
            }

            await sleep(CONFIG.resultCheckInterval);
        }

        return null;
    }

    async function deleteOne() {
        const rows = getRows();

        if (!rows.length) {
            setRunning(false);
            alert(`粉丝勋章已删除完成,共删除 ${getDeleteCount()} 个。`);
            return 'done';
        }

        const firstRow = rows[0];
        const beforeCount = rows.length;
        const beforeName = getRowName(firstRow);

        log('准备删除第一个勋章:', beforeName);

        const clicked = clickDeleteInRow(firstRow);
        if (!clicked) {
            setRunning(false);
            alert('删除失败:没找到第一项的删除按钮。');
            return 'fail';
        }

        await sleep(CONFIG.afterDeleteClickDelay);

        const ok = await processNoticeDialog();
        if (!ok) {
            setRunning(false);
            alert('删除失败:没找到确认弹窗或确认按钮。');
            return 'fail';
        }

        setDeleteCount(getDeleteCount() + 1);
        log(`已提交第 ${getDeleteCount()} 次删除`);

        const result = await waitDeleteResult(beforeName, beforeCount);

        if (!result) {
            setRunning(false);
            alert('删除后既没有刷新,也没有检测到列表变化,脚本已停止。');
            return 'fail';
        }

        return result;
    }

    async function runLoop() {
        while (isRunning() && !isStopped()) {
            if (getDeleteCount() >= CONFIG.maxDeleteCount) {
                setRunning(false);
                alert(`已达到最大保护次数 ${CONFIG.maxDeleteCount},脚本已停止。`);
                return;
            }

            const rows = getRows();
            if (!rows.length) {
                setRunning(false);
                alert(`粉丝勋章已删除完成,共删除 ${getDeleteCount()} 个。`);
                return;
            }

            const result = await deleteOne();

            if (result === 'reload') {
                log('等待页面刷新后续跑');
                return;
            }

            if (result === 'done') {
                return;
            }

            if (result === 'fail') {
                return;
            }

            log('当前页继续下一轮删除,结果类型:', result);
            await sleep(1500);
        }
    }

    function createButtons() {
        if (!document.getElementById('tm-bili-medal-auto-delete-start')) {
            const startBtn = document.createElement('button');
            startBtn.id = 'tm-bili-medal-auto-delete-start';
            startBtn.textContent = '开始自动删除';
            Object.assign(startBtn.style, {
                position: 'fixed',
                right: '20px',
                bottom: '120px',
                zIndex: '999999',
                padding: '10px 14px',
                background: '#e74c3c',
                color: '#fff',
                border: 'none',
                borderRadius: '8px',
                cursor: 'pointer',
                fontSize: '14px',
                boxShadow: '0 2px 8px rgba(0,0,0,.25)'
            });

            startBtn.onclick = async () => {
                resetState();
                setRunning(true);
                await startRun();
            };

            document.body.appendChild(startBtn);
        }

        if (!document.getElementById('tm-bili-medal-auto-delete-stop')) {
            const stopBtn = document.createElement('button');
            stopBtn.id = 'tm-bili-medal-auto-delete-stop';
            stopBtn.textContent = '停止自动删除';
            Object.assign(stopBtn.style, {
                position: 'fixed',
                right: '20px',
                bottom: '70px',
                zIndex: '999999',
                padding: '10px 14px',
                background: '#555',
                color: '#fff',
                border: 'none',
                borderRadius: '8px',
                cursor: 'pointer',
                fontSize: '14px',
                boxShadow: '0 2px 8px rgba(0,0,0,.25)'
            });

            stopBtn.onclick = () => {
                setStop(true);
                setRunning(false);
                releaseLock();
                alert('已停止自动删除。');
            };

            document.body.appendChild(stopBtn);
        }
    }

    async function startRun() {
        if (!acquireLock()) {
            log('已有一个删除流程在运行,跳过');
            return;
        }

        try {
            await waitTableReady();
            await sleep(CONFIG.pageReadyDelay);

            if (!isRunning() || isStopped()) return;
            await runLoop();
        } finally {
            if (!pageUnloading) {
                releaseLock();
            }
        }
    }

    window.addEventListener('load', async () => {
        createButtons();
        setInterval(createButtons, 2000);

        if (CONFIG.autoStart && isRunning() && !isStopped()) {
            log('检测到上次任务未完成,自动续跑');
            await startRun();
        }
    });
})();