// ==UserScript==
// @name 🚀 Delete AI Conversation
// @namespace https://github.com/xianghongai/Tampermonkey-UserScript
// @version 1.0.1
// @description Delete AI conversation quickly and directly. Support shortcut key(Alt+Command+Backspace)
// @description:zh-CN 快速/直接删除 AI 对话,支持快捷键(Alt+Command+Backspace)
// @description:zh-TW 快速/直接删除 AI 對話,支持快捷鍵(Alt+Command+Backspace)
// @description:ja-JP AI 会話を迅速に/直接に削除します。ショートカットキー(Alt+Command+Backspace)をサポートします。
// @description:ko-KR AI 대화를 빠르고 직접적으로 삭제합니다. 단축키(Alt+Command+Backspace)를 지원합니다.
// @description:ru-RU Быстро/непосредственно удалить AI-диалог. Поддерживает горячую клавишу(Alt+Command+Backspace)
// @description:es-ES Eliminar rápidamente/ directamente una conversación de IA. Soporta el atajo de teclado(Alt+Command+Backspace)
// @description:fr-FR Supprimer rapidement/ directement une conversation AI. Prise en charge des raccourcis clavier(Alt+Command+Backspace)
// @author Nicholas Hsiang
// @icon https://xinlu.ink/favicon.ico
// @match https://monica.im/home*
// @match https://grok.com/*
// @match https://chatgpt.com/c/*
// @match https://chat.deepseek.com/*
// @match https://gemini.google.com/*
// @grant GM.xmlHttpRequest
// @grant GM_addStyle
// @run-at document-end
// @license MIT
// ==/UserScript==
(function () {
'use strict';
console.log(GM_info.script.name);
// 两种删除交互
// 1. 通过右下角按钮,删除当前页面的对话
// 2. 在对话列表中,点击删除按钮,删除对应的对话
// 两种删除方式
// 1. API:通过服务删除
// 2. UI:借助原站点 UI 删除交互
// 两种获取初始对话数据方式
// 1. ID:从 URL 中获取
// 2. TITLE:从对话标题中获取 (借助 Developer Tools + getElementPath(temp) 获取元素路径)
// 模式
const MODE = {
ID_API: 'id_api',
ID_UI: 'id_ui',
TITLE_UI: 'title_ui',
};
const CONFIG = {
'https://gemini.google.com': {
mode: MODE.ID_UI,
conversation_url_pattern: 'https://gemini.google.com/app/{id}',
conversation_item_selector: '.conversation-items-container',
conversation_item_action_selector: '.conversation-actions-menu-button',
conversation_item_action_menu_item_selector: '[data-test-id="delete-button"]',
delete_confirm_modal_button_selector: '[data-test-id="confirm-button"]',
getConversationItemById(conversationId) {
const conversationItems = Array.from(document.querySelectorAll('.conversation-items-container'));
const conversationItem = conversationItems.find((item) => {
const conversationIdElement = item.querySelector('.mat-mdc-tooltip-trigger.conversation');
return conversationIdElement?.getAttribute('jslog').includes(conversationId);
});
return conversationItem;
},
},
'https://grok.com': {
mode: MODE.ID_API,
api_url: 'https://grok.com/rest/app-chat/conversations/soft/{id}',
method: 'DELETE',
// 以从 URL 中提取对话 ID
conversation_url_pattern: 'https://grok.com/chat/{id}',
// 获取所有"对话项"的 CSS 选择器。不能带层级。
// THINK: 通过选择器,可以做两件事:
// 1. 获取所有对话项;
// 2. 点击内部操作,获取该对话项。
conversation_item_selector: '[data-sidebar="menu-button"][href^="/chat/"]',
// 从对话项中获取携带对话 ID 元素
getConversationIdElement(conversationItem) {
const conversationIdElement = conversationItem.querySelector('[href^="/chat/"]') || conversationItem;
return conversationIdElement;
},
// 获取对话 ID,通过对话项中的元素 (对话项,或其内部元素)
getConversationIdFormItem(element) {
return element.getAttribute('href').split('/chat/')[1]
},
},
'https://monica.im': {
mode: MODE.ID_UI,
conversation_url_pattern: '?convId={id}',
conversation_item_selector: '[class^="conversation-name-item-wrapper"]',
conversation_item_action_selector: '[class^="popover-content-wrapper"]',
conversation_item_action_menu_item_selector: '[class^="dropdown-menu-item"]',
delete_confirm_modal_button_selector: '[class^="monica-btn"]',
// 通过对话 ID 获取对话项元素,以进入更多菜单
getConversationItemById(conversationId) {
const conversationItemSelector = '[href$="{id}"]'.replace('{id}', conversationId);
return document.querySelector(conversationItemSelector);
},
getConversationIdFormQueryValue(queryValue) {
return queryValue.split('conv:')[1];
},
},
'https://chatgpt.com': {
mode: MODE.ID_API,
api_url: 'https://chatgpt.com/backend-api/conversation/{id}',
method: 'PATCH',
api_body: JSON.stringify({
is_visible: false,
conversation_id: '{id}',
}),
need_authorization: true,
conversation_url_pattern: 'https://chatgpt.com/c/{id}',
// THINK:不通过选择器,需要两个函数分别处理
// 获取所有对话项
getConversationItems() {
return Array.from(document.querySelectorAll('.group.__menu-item')).filter((item) => item.getAttribute('href')?.startsWith('/c/'));
},
// 点击内部操作,获取该对话项
getConversationItem(event) {
const target = event.target;
const conversationItem = parent(target, '.group.__menu-item');
return conversationItem;
},
getConversationIdElement(conversationItem) {
return conversationItem;
},
getConversationIdFormItem(element) {
return element.getAttribute('href').split('/c/')[1];
},
},
'https://chat.deepseek.com': {
mode: MODE.TITLE_UI,
conversation_url_pattern: 'https://chat.deepseek.com/a/chat/s/{id}',
conversation_item_selector: 'html > body:nth-child(2) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div:nth-child(1) > div:nth-child(1) > div:nth-child(3) > div:nth-child(1) > div > div[tabindex]',
conversation_item_action_selector: 'div[tabindex]',
conversation_item_action_menu_item_selector: '.ds-dropdown-menu-option.ds-dropdown-menu-option--error',
delete_confirm_modal_button_selector: '.ds-modal-content .ds-button.ds-button--error',
getConversationTitle() {
return document.querySelector('html > body:nth-child(2) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div:nth-child(3) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)').textContent.trim();
},
},
// TODO: document_title_ui/document_title_api 模式
// 1. 获取 Document Title
// 2. 获取 Document 中的所有对话项
// 3. 遍历对话项,找到与 Document Title 匹配的对话项
// 4. 点击对话项的操作按钮
// 5. 遍历对话项的操作菜单项,找到删除按钮
// 6. 点击删除按钮
// 7. 点击确认删除按钮
};
let config = null;
// 添加删除状态跟踪变量
let isDeleting = false;
// 保存删除按钮引用
let deleteButton = null;
function getConfig() {
if (config) {
return config;
}
config = CONFIG[window.location.origin];
if (!config) {
throw new Error(`未找到当前网站的配置: ${window.location.origin}`);
}
return config;
}
function getConversationIdFormUrl() {
const currentUrl = window.location.href;
const config = getConfig();
const { conversation_url_pattern, getConversationIdFormQueryValue } = config;
// 如果模式以?开头,表示从查询参数中获取
if (conversation_url_pattern.startsWith('?')) {
const paramName = conversation_url_pattern.slice(1, conversation_url_pattern.indexOf('='));
const urlParams = new URLSearchParams(window.location.search);
const paramValue = urlParams.get(paramName);
if (!paramValue) {
console.error(`未找到查询参数: ${paramName}`);
return null;
}
if (typeof getConversationIdFormQueryValue === 'function') {
return getConversationIdFormQueryValue(paramValue);
}
return paramValue;
}
// 否则从路径中提取
else {
const pattern = conversation_url_pattern.replace('{id}', '(.+)');
const regex = new RegExp(pattern);
const match = currentUrl.match(regex);
if (match && match[1]) {
return match[1];
}
console.error('无法从URL中提取对话ID');
return null;
}
}
function deleteForIdUi(conversationItem) {
setButtonLoading(true);
const { conversation_item_action_selector, conversation_item_action_menu_item_selector, delete_confirm_modal_button_selector } = getConfig();
if (!conversationItem) {
notification('未找到对话项', { type: 'error' });
setButtonLoading(false);
return;
}
const itemActionElement = conversationItem.querySelector(conversation_item_action_selector);
if (itemActionElement) {
// 点击对话操作按钮
itemActionElement.click();
setTimeout(() => {
// 更多菜单列表项
const menuItemElements = document.querySelectorAll(conversation_item_action_menu_item_selector);
let foundDeleteButton = false;
for (let i = 0; i < menuItemElements.length; i++) {
const menuItemElement = menuItemElements[i];
const menuItemText = menuItemElement.textContent.trim();
const isDeleteElement = menuItemText.includes('Delete') || menuItemText.includes('删除');
if (isDeleteElement) {
foundDeleteButton = true;
// 点击删除按钮
menuItemElement.click();
// 需要“确认删除”
if (delete_confirm_modal_button_selector) {
setTimeout(() => {
const confirmModalButtonElements = document.querySelectorAll(delete_confirm_modal_button_selector);
let foundConfirmButton = false;
for (let i = 0; i < confirmModalButtonElements.length; i++) {
const confirmModalButtonElement = confirmModalButtonElements[i];
const confirmModalButtonText = confirmModalButtonElement.textContent.trim();
const isDeleteElement = confirmModalButtonText.includes('Delete') || confirmModalButtonText.includes('删除');
if (isDeleteElement) {
foundConfirmButton = true;
// 点击确认删除按钮
confirmModalButtonElement.click();
notification('已发起删除请求');
// 在操作完成后延迟重置按钮状态
setButtonLoading(false);
break;
}
}
if (!foundConfirmButton) {
notification('未找到确认按钮', { type: 'error' });
setButtonLoading(false);
}
}, 500);
}
// 没有确认按钮,直接删除
else {
notification('已发起删除请求');
// 在操作完成后延迟重置按钮状态
setButtonLoading(false);
}
break;
}
}
if (!foundDeleteButton) {
notification('未找到删除按钮', { type: 'error' });
setButtonLoading(false);
}
}, 500);
} else {
notification('未找到操作按钮', { type: 'error' });
setButtonLoading(false);
}
}
async function deleteForIdApi(conversationId) {
setButtonLoading(true);
if (!conversationId) {
notification('无法获取对话ID', { type: 'error' });
setButtonLoading(false);
return;
}
const { api_url, method, api_body, need_authorization } = getConfig();
// 请求 URL 中替换 {id} 为对话 ID
const url = api_url.replace('{id}', conversationId);
const headers = {
'Content-Type': 'application/json',
};
let cacheAuthorization = null;
const cacheAuthorizationKey = `authorization__${window.location.origin}`;
if (need_authorization) {
// 从 localStorage 中获取 Authorization 值
cacheAuthorization = localStorage.getItem(cacheAuthorizationKey);
if (cacheAuthorization) {
headers.Authorization = cacheAuthorization;
} else {
// 提示用户提供 Authorization 值
const authorization = prompt('请输入 Authorization 值 (将通过 localStorage 缓存,下次删除时无需再次输入)');
if (authorization) {
headers.Authorization = authorization;
} else {
notification('请输入 Authorization 值', { type: 'error' });
setButtonLoading(false);
return;
}
}
}
try {
if (method === 'DELETE') {
const response = await fetch(url, {
method,
});
if (response.ok) {
notification('已发起删除请求,通过 API 删除,需要刷新页面查看结果');
} else {
console.error(response);
notification('删除出现异常,请查看 Developer Tools 中的 Console 输出、检查 Network 请求', { type: 'error' });
}
} else if (method === 'PATCH') {
const response = await fetch(url, {
method,
body: api_body.replace('{id}', conversationId),
headers,
});
if (response.ok) {
notification('已发起删除请求,通过 API 删除,需要刷新页面查看结果');
if (need_authorization) {
// 缓存 Authorization 值,以当前网站的 origin 为 key
localStorage.setItem(cacheAuthorizationKey, headers.Authorization);
}
} else {
console.error(response);
notification('删除出现异常,请查看 Developer Tools 中的 Console 输出、检查 Network 请求', { type: 'error' });
}
}
} catch (error) {
console.error(error);
notification('删除请求失败: ' + error.message, { type: 'error' });
} finally {
// 无论成功或失败,都在1秒后恢复按钮状态
setTimeout(() => {
setButtonLoading(false);
}, 1000);
}
}
function deleteForTitleUi(conversationTitle) {
const config = getConfig();
const {
conversation_item_selector,
conversation_item_action_selector,
conversation_item_action_menu_item_selector,
delete_confirm_modal_button_selector
} = config;
const conversationItemElements = document.querySelectorAll(conversation_item_selector);
for (let i = 0; i < conversationItemElements.length; i++) {
const conversationItemElement = conversationItemElements[i];
const conversationItemText = conversationItemElement.textContent.trim();
if (conversationItemText === conversationTitle) {
const conversationItemActionElement = conversationItemElement.querySelector(conversation_item_action_selector);
// 点击对话操作按钮
conversationItemActionElement.click();
setTimeout(() => {
const conversationItemActionMenuElements = document.querySelectorAll(conversation_item_action_menu_item_selector);
for (let j = 0; j < conversationItemActionMenuElements.length; j++) {
const conversationItemActionMenuElement = conversationItemActionMenuElements[j];
const conversationItemActionMenuElementText = conversationItemActionMenuElement.textContent.trim();
if (conversationItemActionMenuElementText.includes('Delete') || conversationItemActionMenuElementText.includes('删除')) {
// 点击删除按钮
conversationItemActionMenuElement.click();
setTimeout(() => {
const deleteConfirmModalButtonElements = document.querySelectorAll(delete_confirm_modal_button_selector);
for (let k = 0; k < deleteConfirmModalButtonElements.length; k++) {
const deleteConfirmModalButtonElement = deleteConfirmModalButtonElements[k];
const deleteConfirmModalButtonElementText = deleteConfirmModalButtonElement.textContent.trim();
if (deleteConfirmModalButtonElementText.includes('Delete') || deleteConfirmModalButtonElementText.includes('删除')) {
// 点击确认删除按钮
deleteConfirmModalButtonElement.click();
notification('已删除对话');
break;
}
}
}, 500);
break;
}
}
}, 500);
break;
}
}
}
function handleDelete() {
if (isDeleting) {
notification('正在处理删除请求,请稍候...', { type: 'warning' });
return;
}
const config = getConfig();
if (!config) {
notification('无法获取配置信息', { type: 'error' });
return;
}
const { mode, getConversationTitle, getConversationItemById } = config;
const conversationId = getConversationIdFormUrl();
if (mode === MODE.ID_API) {
deleteForIdApi(conversationId);
} else if (mode === MODE.ID_UI) {
deleteForIdUi(getConversationItemById(conversationId));
} else if (mode === MODE.TITLE_UI) {
deleteForTitleUi(getConversationTitle());
}
}
function createElement() {
const wrap = document.createElement('div');
wrap.className = 'x-conversation-action-wrap';
// 删除按钮
const btn = document.createElement('button');
btn.textContent = 'Delete';
btn.className = 'x-conversation-action x-conversation-delete';
btn.onclick = handleDelete;
deleteButton = btn;
wrap.appendChild(btn);
// 侦测对话项,添加删除按钮
const inspectConversationItemBtn = document.createElement('button');
inspectConversationItemBtn.textContent = 'Inspect';
inspectConversationItemBtn.className = 'x-conversation-action x-conversation-inspect';
inspectConversationItemBtn.onclick = createRemoveActionForConversationItem;
wrap.appendChild(inspectConversationItemBtn);
document.body.appendChild(wrap);
}
// 为每个对话项添加删除按钮
function createRemoveActionForConversationItem() {
const config = getConfig();
const { getConversationItems, conversation_item_selector } = config;
let conversationItemElements = []
if (typeof getConversationItems === 'function') {
conversationItemElements = getConversationItems();
} else {
conversationItemElements = document.querySelectorAll(conversation_item_selector);
}
for (let i = 0; i < conversationItemElements.length; i++) {
const conversationItemElement = conversationItemElements[i];
conversationItemElement.style.position = 'relative';
// 是否已经添加过删除按钮
const removeIconElement = conversationItemElement.querySelector('.x-conversation-item-remove');
if (removeIconElement) {
// 切换显示状态
if (removeIconElement.classList.contains('hidden')) {
removeIconElement.classList.remove('hidden');
} else {
removeIconElement.classList.add('hidden');
}
continue;
}
// 添加删除按钮
const iconElement = document.createElement('i');
iconElement.innerHTML = getRemoveIcon();
iconElement.className = 'x-conversation-item-remove';
iconElement.setAttribute('title', 'Delete Conversation');
conversationItemElement.appendChild(iconElement);
}
}
// 添加快捷键监听功能
function setupKeyboardShortcut() {
document.addEventListener('keydown', function (event) {
// 检测 Alt+Command+Backspace 组合键 (macOS)
// 或 Alt+Win+Backspace (Windows)
if (event.altKey && event.metaKey && event.key === 'Backspace') {
event.preventDefault(); // 阻止默认行为
handleDelete();
// 显示快捷键触发提示
notification('通过快捷键触发删除操作');
}
});
}
// 为对话项删除按钮委托事件
function addEventListenerForConversationItem() {
document.addEventListener('click', function (event) {
const target = event.target;
const { mode, conversation_item_selector, getConversationItem, getConversationIdElement, getConversationIdFormItem } = getConfig();
if (matches(target, '.x-conversation-item-remove')) {
// 1. 能从对话项中获取到对话 ID 的网站
if (mode === MODE.ID_UI) {
event.stopPropagation();
event.preventDefault();
const conversationItem = parent(target, conversation_item_selector);
deleteForIdUi(conversationItem);
return;
} else if (mode === MODE.ID_API) {
event.stopPropagation();
event.preventDefault();
let conversationItem = null;
if (typeof getConversationItem === 'function') {
conversationItem = getConversationItem(event);
} else if (typeof conversation_item_selector === 'string') {
conversationItem = parent(target, conversation_item_selector);
}
const conversationIdElement = getConversationIdElement(conversationItem);
const conversationId = getConversationIdFormItem(conversationIdElement);
deleteForIdApi(conversationId);
return;
}
// 2. 不能从对话项中获取对话 ID 的网站,只能将事件先冒泡,进入对话项之后,再调用通过 URL 删除逻辑
setTimeout(() => {
handleDelete();
}, 1000);
}
});
}
function main() {
createStyle();
createElement();
setupKeyboardShortcut();
addEventListenerForConversationItem();
}
main();
// 添加通知
function notification(message, { type = 'success', duration = 5000 } = {}) {
const notification = document.createElement('div');
notification.textContent = message;
notification.className = `x-conversation-delete-notification ${type}`;
// 移除旧提示
const existing = document.querySelector('.x-conversation-delete-notification');
if (existing) existing.remove();
document.body.appendChild(notification);
setTimeout(() => notification.remove(), duration);
}
// 设置按钮为加载状态
function setButtonLoading(loading) {
if (!deleteButton) return;
isDeleting = loading;
if (loading) {
deleteButton.classList.add('loading');
deleteButton.textContent = '正在删除...';
deleteButton.disabled = true;
} else {
deleteButton.classList.remove('loading');
deleteButton.textContent = 'Delete';
deleteButton.disabled = false;
}
}
function createStyle() {
// 初始化
GM_addStyle(`
.x-conversation-action-wrap {
position: fixed;
bottom: 18px;
right: 18px;
z-index: 99999;
display: flex;
gap: 8px;
align-items: center;
}
.x-conversation-action {
padding: 4px 8px;
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.3s ease;
}
.x-conversation-inspect {
background:rgb(255, 163, 24);
}
.x-conversation-inspect:hover {
background: #ff9800;
}
.x-conversation-delete {
background: #ff4444;
color: white;
}
.x-conversation-delete:disabled {
cursor: not-allowed;
opacity: 0.7;
}
.x-conversation-delete.loading {
background: #999;
position: relative;
padding-right: 24px; /* 为加载图标留出空间 */
}
.x-conversation-delete.loading::after {
content: "";
position: absolute;
width: 10px;
height: 10px;
top: 50%;
right: 8px;
margin-top: -5px;
border: 2px solid rgba(255, 255, 255, 0.5);
border-top-color: white;
border-radius: 50%;
animation: loader-spin 1s linear infinite;
}
.x-conversation-delete-notification {
position: fixed;
bottom: 62px;
right: 18px;
padding: 4px 8px;
border-radius: 4px;
color: white;
font-family: system-ui;
font-size: 12px;
animation: slideIn 0.3s;
z-index: 99999;
}
.x-conversation-item-remove {
position: absolute;
left: 100%;
transform: translateX(-60px) translateY(-50%);
top: 50%;
display: block;
width: 18px;
height: 18px;
line-height: 18px;
cursor: pointer;
}
.x-conversation-item-remove.hidden {
display: none;
}
.x-conversation-delete-notification.success { background: #4CAF50; }
.x-conversation-delete-notification.error { background: #ff4444; }
.x-conversation-delete-notification.warning { background: #ff9800; }
@keyframes slideIn {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes loader-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`);
}
function getRemoveIcon() {
const removeIcon = `<?xml version="1.0" encoding="UTF-8"?><svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18.4237 10.5379C18.794 10.1922 19.2817 10 19.7883 10H42C43.1046 10 44 10.8954 44 12V36C44 37.1046 43.1046 38 42 38H19.7883C19.2817 38 18.794 37.8078 18.4237 37.4621L4 24L18.4237 10.5379Z" fill="#d0021b" stroke="#d0021b" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M36 19L26 29" stroke="#FFF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M26 19L36 29" stroke="#FFF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
return removeIcon;
}
// 获取元素的绝对路径作为 CSS 选择器,用于 DevTools
function getElementPath(element) {
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
return '';
}
const path = [];
while (element && element.nodeType === Node.ELEMENT_NODE) {
let selector = element.tagName.toLowerCase();
const parent = element.parentElement;
if (parent) {
const siblings = Array.from(parent.children);
const index = siblings.indexOf(element) + 1;
selector += `:nth-child(${index})`;
}
path.unshift(selector);
element = parent;
}
return path.join(' > ');
}
/**
* 判断当前元素及父元素是否匹配给定的 CSS 选择器
* @param {Element} currentElement 当前元素
* @param {string} selector CSS 选择器
* @returns {boolean} 是否匹配
*/
function matches(currentElement, selector) {
while (currentElement !== null && currentElement !== document.body) {
if (currentElement.matches(selector)) {
return true;
}
currentElement = currentElement.parentElement;
}
// 检查 body 元素
return document.body.matches(selector);
}
/**
* 获取当前元素的父元素,直到找到匹配给定 CSS 选择器的元素
* @param {Element} currentElement 当前元素
* @param {string} selector CSS 选择器
* @returns {Element|null} 匹配的父元素或 null
*/
function parent(currentElement, selector) {
for (; currentElement && currentElement !== document; currentElement = currentElement.parentNode) {
if (currentElement.matches(selector)) return currentElement;
}
return null;
}
})();