DeepSeek quick delete

侧栏 hover 显示快速删除;不改动原生 ⋯ 菜单

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         DeepSeek quick delete
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  侧栏 hover 显示快速删除;不改动原生 ⋯ 菜单
// @match        https://chat.deepseek.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  const STYLE_ID = 'tm-ds-qdel-style';
  const BTN_CLS = 'tm-ds-qdel-btn';
  const HOST_CLS = 'tm-ds-qdel-host';
  const MARK = 'tmDsQdel';
  const deletedIds = new Set();

  function ensureStyle() {
    if (document.getElementById(STYLE_ID)) return;
    const style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = `
      a.${HOST_CLS} {
        position: relative !important;
      }
      .${BTN_CLS} {
        position: absolute;
        right: 32px;              /* 越大越靠左;对应以前 margin-right:32px 的间距感 */
        top: 50%;
        transform: translateY(-50%);
        z-index: 6;
        width: 22px;
        height: 22px;
        line-height: 20px;
        text-align: center;
        border-radius: 6px;
        color: #f66;
        cursor: pointer;
        font-size: 16px;
        user-select: none;
        opacity: 0;
        pointer-events: none;
        transition: opacity .12s ease, background .12s ease;
      }
      a.${HOST_CLS}:hover .${BTN_CLS} {
        opacity: 1;
        pointer-events: auto;
      }
      .${BTN_CLS}:hover {
        opacity: 1 !important;
        pointer-events: auto !important;
        background: rgba(255,80,80,.14);
      }
    `;
    document.documentElement.appendChild(style);
  }

  function toast(msg) {
    console.error('[ds-qdel]', msg);
    const el = document.createElement('div');
    el.textContent = msg;
    el.style.cssText =
      'position:fixed;z-index:2147483647;right:16px;bottom:16px;max-width:360px;' +
      'padding:10px 14px;border-radius:8px;background:#222;color:#fff;font:13px/1.4 sans-serif;';
    document.body.appendChild(el);
    setTimeout(() => el.remove(), 3200);
  }

  function getId(a) {
    const m = (a.getAttribute('href') || '').match(/\/a\/chat\/s\/([a-f0-9-]+)/i);
    return m ? m[1] : null;
  }

  function pickLinks() {
    return [...document.querySelectorAll('a[href*="/a/chat/s/"]')].filter(getId);
  }

  function getToken() {
    for (const k of ['userToken', 'USER_TOKEN', 'token']) {
      const raw = localStorage.getItem(k);
      if (!raw) continue;
      try {
        const p = JSON.parse(raw);
        const t = typeof p === 'object' && p ? (p.value || p.token || p.accessToken) : p;
        if (t) return String(t);
      } catch {
        if (raw.length > 10) return raw;
      }
    }
    throw new Error('未找到 userToken,请先登录');
  }

  async function deleteChat(id) {
    const token = getToken();
    const r = await fetch('/api/v0/chat_session/delete', {
      method: 'POST',
      credentials: 'include',
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
        Accept: '*/*',
        'X-Client-Platform': 'web',
        'X-Client-Locale': 'zh_CN',
        'X-App-Version': '2025.04.25',
      },
      body: JSON.stringify({ chat_session_id: id }),
    });
    const text = await r.text();
    let j = null;
    try { j = JSON.parse(text); } catch { /* ignore */ }
    if (!r.ok) throw new Error(`HTTP ${r.status} ${text.slice(0, 120)}`);
    const code = j?.code ?? j?.data?.biz_code ?? j?.biz_code;
    if (code != null && Number(code) !== 0) {
      throw new Error(j?.msg || j?.message || `code=${code}`);
    }
  }

  function findChatRow(a) {
    let el = a;
    while (el.parentElement) {
      const p = el.parentElement;
      if (p === document.body || p === document.documentElement) break;
      if (p.matches('nav, aside, [role="navigation"]')) break;
      const others = pickLinks().filter(x => x !== a && p.contains(x));
      if (others.length) return el;
      el = p;
    }
    return el;
  }

  function hideChatRow(a) {
    const row = findChatRow(a);
    row.style.setProperty('display', 'none', 'important');
    return row;
  }

  function showChatRow(row) {
    if (!row) return;
    row.style.removeProperty('display');
  }

  function goHomeSoft() {
    const btn = [
      ...document.querySelectorAll('a[href="/"], a[href="/a/chat"], a[href="/a/chat/"], button, div[role="button"]'),
    ].find(el => {
      const tx = (el.getAttribute('aria-label') || el.textContent || '').trim();
      return (
        el.matches('a[href="/"], a[href="/a/chat"], a[href="/a/chat/"]') ||
        /新对话|新聊天|new chat|开启新对话/i.test(tx)
      );
    });
    if (btn) btn.click();
    else location.assign('https://chat.deepseek.com/');
  }

  function onDeleteClick(e, a, btn, id) {
    e.preventDefault();
    e.stopPropagation();
    e.stopImmediatePropagation();
    if (btn.dataset.busy) return;
    btn.dataset.busy = '1';

    const viewing = location.pathname.includes(`/a/chat/s/${id}`);
    deletedIds.add(id);
    const row = hideChatRow(a);
    if (viewing) goHomeSoft();

    deleteChat(id)
      .catch(err => {
        deletedIds.delete(id);
        showChatRow(row);
        delete a.dataset[MARK];
        toast(String(err?.message || err));
      })
      .finally(() => {
        delete btn.dataset.busy;
        btn.textContent = '×';
      });
  }

  function attach(a) {
    const id = getId(a);
    if (!id) return;

    if (deletedIds.has(id)) {
      hideChatRow(a);
      return;
    }
    if (a.dataset[MARK]) return;
    a.dataset[MARK] = '1';
    a.classList.add(HOST_CLS);

    const btn = document.createElement('span');
    btn.className = BTN_CLS;
    btn.textContent = '×';
    btn.title = 'Delete';

    // mousedown + click 双保险,避免被 <a> 导航抢走
    const handler = e => onDeleteClick(e, a, btn, id);
    btn.addEventListener('mousedown', handler, true);
    btn.addEventListener('click', handler, true);

    // 挂到 <a> 自身,不进原生 ⋯ 容器
    a.appendChild(btn);
  }

  function scan() {
    ensureStyle();
    pickLinks().forEach(attach);
  }

  let timer = 0;
  new MutationObserver(() => {
    if (timer) return;
    timer = setTimeout(() => {
      timer = 0;
      scan();
    }, 50);
  }).observe(document.documentElement, { childList: true, subtree: true });

  scan();
})();