您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Pin/Unpin ChatGPT chats with local-storage persistence
// ==UserScript== // @name Pin ChatGPT Chats // @namespace Violentmonkey Scripts // @match https://chatgpt.com/* // @grant none // @version 1.0 // @esversion 10 // @description Pin/Unpin ChatGPT chats with local-storage persistence // @license MIT // ==/UserScript== (function () { const STORAGE_KEY = "pinnedChats"; let scheduled = false; let lastPins = JSON.stringify(getPinnedChats()); const processedChats = new WeakSet(); let pinnedHeader = null; // --- Storage Helpers --- function getPinnedChats() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || []; } catch (err) { return []; } } function savePinnedChats(list) { localStorage.setItem(STORAGE_KEY, JSON.stringify(list)); } function isPinned(chatId) { return getPinnedChats().includes(chatId); } function togglePin(chatId) { let pins = getPinnedChats(); if (pins.includes(chatId)) { pins = pins.filter(x => x !== chatId); } else { pins.push(chatId); } savePinnedChats(pins); lastPins = JSON.stringify(pins); sortChats(); } // --- Utility: Extract Chat ID from href --- function extractChatId(href) { if (!href) return null; const match = href.match(/\/c\/([^/?#]+)/); return match ? match[1] : null; } // Inject style for hover behavior const style = document.createElement("style"); style.textContent = ` .chat-pin.unpinned { visibility: hidden; } a:hover .chat-pin.unpinned { visibility: visible; } `; document.head.appendChild(style); // --- Create Pin Button --- function createPin(chatId, pinned) { const span = document.createElement("span"); span.innerText = pinned ? "★" : "☆"; span.className = "chat-pin" + (pinned ? " pinned" : " unpinned"); span.style.cursor = "pointer"; span.style.marginLeft = "6px"; span.style.fontSize = "14px"; span.style.userSelect = "none"; span.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); togglePin(chatId); span.replaceWith(createPin(chatId, !pinned)); }); return span; } // --- Add Pins to Chats --- function addPins(container) { const chats = container.querySelectorAll("a[href^='/c/']"); chats.forEach(chat => { if (processedChats.has(chat)) return; const chatId = extractChatId(chat.getAttribute("href")); if (!chatId) return; chat.append(createPin(chatId, isPinned(chatId))); processedChats.add(chat); }); } // --- Create/Ensure Pinned Header --- function ensurePinnedHeader(chatContainer) { if (!pinnedHeader) { pinnedHeader = document.createElement("div"); pinnedHeader.textContent = "📌 Pinned"; pinnedHeader.style.fontWeight = "bold"; pinnedHeader.style.margin = "8px 0"; pinnedHeader.style.padding = "4px 8px"; pinnedHeader.style.background = "rgba(0,0,0,0.05)"; pinnedHeader.style.borderRadius = "6px"; } if (!chatContainer.contains(pinnedHeader)) { chatContainer.prepend(pinnedHeader); } } // --- Sort Chats with Header --- function sortChats() { const chatContainer = document.querySelectorAll("aside")[2]; if (!chatContainer) return; const chats = Array.from(chatContainer.querySelectorAll("a[href^='/c/']")); const pins = getPinnedChats(); const pinnedChats = chats.filter(c => pins.includes(extractChatId(c.getAttribute("href")))); const unpinnedChats = chats.filter(c => !pins.includes(extractChatId(c.getAttribute("href")))); chatContainer.innerHTML = ""; // clear list if (pinnedChats.length > 0) { ensurePinnedHeader(chatContainer); chatContainer.appendChild(pinnedHeader); // Add pinned chats pinnedChats.forEach(c => chatContainer.appendChild(c)); // Separator line const hr = document.createElement("hr"); hr.style.border = "none"; hr.style.borderTop = "1px solid rgba(255,255,255,0.8)"; hr.style.margin = "6px 0"; chatContainer.appendChild(hr); } // Add remaining chats unpinnedChats.forEach(c => chatContainer.appendChild(c)); } // --- Observer (throttled) --- function update() { scheduled = false; const chatContainer = document.querySelectorAll("aside")[2]; if (!chatContainer) return; addPins(chatContainer); const currentPins = JSON.stringify(getPinnedChats()); if (currentPins !== lastPins) { lastPins = currentPins; sortChats(); } } const observer = new MutationObserver(() => { if (!scheduled) { scheduled = true; requestAnimationFrame(update); } }); observer.observe(document.body, { childList: true, subtree: true }); // --- Initial Render on Page Load --- function init() { const chatContainer = document.querySelectorAll("aside")[2]; if (!chatContainer) { requestAnimationFrame(init); return; } addPins(chatContainer); sortChats(); // immediately reorder and add header on reload } init(); })();