批量删除推文,无需任何扩展

一段免费脚本,粘贴到 Chrome 控制台即可清理您在 X 上的旧帖子:先模拟运行、保护最近的推文、限量分批删除。无需扩展,无需第三方服务,任何数据都不会离开您的浏览器。

开始之前:删除一条推文是永久性的。脚本默认以模拟模式提供(DRY_RUN = true):它会列出将被删除的内容,但不动任何东西。请先下载您的 X 存档,核对模拟结果,然后再切换到真正的删除。X 的使用条款并未预设对网页界面的自动化操作:请慢慢来,风险自负。

操作步骤

  1. 打开您的主页:前往 x.com/你的用户名,切到 帖子 标签页,并保持登录状态。
  2. 打开控制台:按 F12(Mac 上按 Cmd+Option+J),然后点击 Console(控制台) 标签。
  3. 配置脚本:复制下方代码,把 ni_de_yonghu 替换成您的 X 用户名(不带 @),调整 KEEP_RECENT(要保留的最近推文数量),如有需要,还可在 KEEP_STATUS_IDS 中列出永不删除的推文 ID。
  4. 先模拟:把脚本粘贴到控制台并按回车确认。在 DRY_RUN = true(默认设置)下,它只会列出将被删除的推文清单,不会真的删除任何内容。
  5. 再删除:如果模拟结果符合预期,把 DRY_RUN 改成 false 并重新运行。脚本每次执行最多删除 40 条推文,每次删除之间暂停 6 到 14 秒。
  6. 重复运行:按需多次重新运行脚本,分批处理剩下的推文。

脚本

Chrome 控制台 · x.com
(() => {
  // ============ 配置 ============
  const HANDLE          = "ni_de_yonghu"; // 你的 X 用户名,不带 @
  const KEEP_RECENT     = 15;           // 永远不删最近的 15 条推文(顶部的那些)
  const KEEP_STATUS_IDS = [             // 额外保护(可选):永不删除的推文 ID
    "1234567890123456789",
  ];
  let   DRY_RUN         = true;         // true = 模拟(仅列出)。false = 真正删除。
  const MIN_DELAY_MS    = 6000;
  const MAX_DELAY_MS    = 14000;
  const MAX_DELETIONS   = 40;           // 每次执行的上限(重新运行以继续)
  // ================================

  const me = HANDLE.toLowerCase();
  const KEEP = new Set(KEEP_STATUS_IDS.map(String));
  const protectedRecent = new Set();
  const RX_DELETE = /(删除|delete|supprimer)/i;
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
  const rand  = (a, b) => Math.floor(a + Math.random() * (b - a));
  const skipped = new Set();

  if (!location.pathname.toLowerCase().startsWith("/" + me)) {
    console.warn(`%c[停止] 请前往 https://x.com/${HANDLE}(帖子标签页)后再运行。`,
      "color:#e0245e;font-weight:bold");
    return;
  }

  const articles = () => [...document.querySelectorAll('article[data-testid="tweet"]')];

  const idOf = (art) => {
    const href = [...art.querySelectorAll('a[href*="/status/"]')]
      .map((x) => x.getAttribute("href"))
      .find((h) => new RegExp(`^/${HANDLE}/status/\\d+`, "i").test(h));
    if (!href) return null;
    const m = href.match(/status\/(\d+)/);
    return m ? m[1] : null;
  };

  // 加载并保护最近的 KEEP_RECENT 条推文(顶部的那些)。
  async function protectRecent() {
    const order = [], seen = new Set();
    let noNew = 0;
    window.scrollTo(0, 0);
    await sleep(1200);
    while (order.length < KEEP_RECENT && noNew < 6) {
      let added = 0;
      for (const art of articles()) {
        const id = idOf(art);
        if (id && !seen.has(id)) { seen.add(id); order.push(id); added++; }
      }
      noNew = added ? 0 : noNew + 1;
      if (order.length >= KEEP_RECENT) break;
      const cs = articles();
      (cs[cs.length - 1] || document.body).scrollIntoView({ block: "end" });
      await sleep(1400);
    }
    for (const id of order.slice(0, KEEP_RECENT)) protectedRecent.add(id);
    return order.slice(0, KEEP_RECENT);
  }

  const eligible = (art) => {
    const id = idOf(art);
    if (!id) return null;
    if (protectedRecent.has(id)) return null;   // 属于最近的推文之一
    if (KEEP.has(id)) return null;              // 额外保护的 ID
    if (skipped.has(id)) return null;
    return id;
  };

  async function deleteOne(art, id) {
    const caret = art.querySelector('[data-testid="caret"]');
    if (!caret) { skipped.add(id); return false; }
    caret.click();
    await sleep(700);
    const del = [...document.querySelectorAll('[role="menuitem"]')]
      .find((i) => RX_DELETE.test((i.innerText || "").trim()));
    if (!del) { document.body.click(); skipped.add(id); return false; }
    del.click();
    await sleep(700);
    const confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
    if (!confirmBtn) { skipped.add(id); return false; }
    confirmBtn.click();
    await sleep(1500);
    return true;
  }

  async function dryScan() {
    const seen = new Set();
    let noNew = 0;
    console.log("%c[模拟] 不会删除任何内容。正在统计……", "color:#1d9bf0;font-weight:bold");
    while (noNew < 4) {
      let added = 0;
      for (const art of articles()) {
        const id = eligible(art);
        if (id && !seen.has(id)) { seen.add(id); added++; }
      }
      noNew = added ? 0 : noNew + 1;
      const cs = articles();
      (cs[cs.length - 1] || document.body).scrollIntoView({ block: "end" });
      await sleep(1200);
    }
    console.log(`%c[模拟] 将会删除 ${seen.size} 条推文(最近的已受保护)。`,
      "color:#1d9bf0;font-weight:bold");
    console.log([...seen].slice(0, 60));
  }

  async function run() {
    let count = 0, guard = 0, noHit = 0, started = false;
    while (count < MAX_DELETIONS && guard++ < 1500) {
      const pair = articles().map((a) => [a, eligible(a)]).find(([, id]) => id);
      if (!pair) {
        if ((started && noHit >= 6) || noHit >= 40) break;
        const cs = articles();
        (cs[cs.length - 1] || document.body).scrollIntoView({ block: "end" });
        await sleep(1500);
        noHit++;
        continue;
      }
      started = true; noHit = 0;
      const [node, id] = pair;
      node.scrollIntoView({ block: "center" });
      await sleep(600);
      const ok = await deleteOne(node, id);
      if (ok) count++;
      console.log(`${ok ? "已删除" : "已跳过"} ${id}  (总计: ${count}/${MAX_DELETIONS})`);
      await sleep(rand(MIN_DELAY_MS, MAX_DELAY_MS));
    }
    console.log(`%c[完成] 本轮已删除 ${count} 条。如果还有剩余,请重新运行脚本。`,
      "color:#00ba7c;font-weight:bold");
  }

  (async () => {
    console.log(`正在加载并保护最近的 ${KEEP_RECENT} 条推文……`);
    const recent = await protectRecent();
    if (recent.length < KEEP_RECENT) {
      console.warn(`%c[停止] 只加载到 ${recent.length} 条推文(X 触发了限流)。请刷新页面(F5)后重试。`,
        "color:#e0245e;font-weight:bold");
      return;
    }
    console.log("%c已保护(最近的推文):", "color:#00ba7c;font-weight:bold", recent);
    if (DRY_RUN) { await dryScan(); return; }
    await run();
  })();
})();

脚本如何保护您

脚本内置了四道防线。首先是默认模拟:只要 DRY_RUN 的值为 true,就不会删除任何内容,您只会得到相关推文的清单。其次是保护最近的推文:脚本会先记住您最近的推文(默认 15 条),永远不去碰它们。再次是白名单:任何放进 KEEP_STATUS_IDS 的 ID 都不可触碰,方便保留您的置顶推文或重要公告。最后是谨慎的节奏:每次删除之间随机暂停 6 到 14 秒,删满 40 条后自动停止,以免触发 X 的限流。

在大扫除之前,别忘了先归档重要的东西:邮箱与网址提取器可以从推文导出文件中取出所有链接,字数统计器能告诉您这些年的发帖分量几何,文本清理器则能把从存档复制粘贴来的文字重新整理干净。至于以后的推文,还有 Unicode 花式文本大小写转换器为您效力。工具箱里的所有工具都和本教程一样在您的浏览器中运行,秉持我们隐私政策的一贯精神。

常见问题

删除操作可以撤销吗?

不能。推文一旦删除便永久消失,X 不提供任何回收站。正因如此,脚本默认以模拟模式提供(DRY_RUN = true):请先原样运行一遍,得到将被删除内容的清单,核对无误后,只有在您完全确定时才把它改成 false。删除前也请先下载您的 X 存档(设置,然后是您的账号,然后是下载数据存档)。

脚本会删除别人的推文吗?

不会。X 只在您自己的帖子上显示删除按钮:脚本只能对当前登录的账号、在您自己的主页上操作。它既不会动别人的回复,也不会碰任何第三方内容。

为什么限制每次 40 条并设置随机延时?

为了对平台温和一些。删除过快或过于密集可能触发 X 的限流,甚至导致账号被临时封禁。脚本在每次删除之间等待 6 到 14 秒,删满 40 条后自动停止:重新运行即可继续分批处理。

X 允许这样做吗?

您删除的是自己的内容,这是您的权利。不过,X 的使用条款并未预设对网页界面的自动化操作:请节制使用本脚本,风险自负。官方的替代方案是手动删除,或使用通过 API 操作的专门服务。

如何保护某些特定的推文?

两重保护可以叠加:KEEP_RECENT 会自动保留您最近的推文(默认 15 条),KEEP_STATUS_IDS 则接受一份永不删除的 ID 清单。推文的 ID 就是其网址末尾的那串长数字:x.com/用户名/status/1234567890123456789。