// ==UserScript== // @name Cookie 管理器 // @namespace https://github.com/examplecode/useful-user-scripts/ // @version 2.0.1 // @description 管理当前网站的Cookies:查看、复制、编辑、添加、删除、导入导出 // @author examplecode // @match *://*/* // @homepage https://github.com/examplecode/useful-user-scripts/ // @grant GM_cookie // @grant GM_registerMenuCommand // @run-at document-idle // @license MIT // ==/UserScript== (function () { 'use strict'; // ==================== 配置 ==================== const CONFIG = { panelWidth: '380px', }; // ==================== 样式(Shadow DOM 内部,无需 !important)==================== const STYLES = ` * { box-sizing: border-box; margin: 0; padding: 0; } :host { all: initial; } .panel { position: fixed; top: 0; right: -${CONFIG.panelWidth}; width: ${CONFIG.panelWidth}; height: 100vh; height: 100dvh; background: #fff; box-shadow: -2px 0 12px rgba(0,0,0,0.15); z-index: 2147483647; transition: right 0.3s ease; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; line-height: 1.5; color: #333; display: flex; flex-direction: column; overflow: hidden; } .panel.open { right: 0; } .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 12px 16px; display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; } .header h3 { font-size: 16px; font-weight: 600; color: white; } .header-actions { display: flex; gap: 8px; } .header-btn { background: rgba(255,255,255,0.2); border: none; color: white; width: 28px; height: 28px; border-radius: 4px; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; } .header-btn:hover { background: rgba(255,255,255,0.35); } .toolbar { padding: 10px 12px; border-bottom: 1px solid #eee; display: flex; gap: 6px; flex-shrink: 0; flex-wrap: wrap; } .btn { padding: 6px 12px; border: 1px solid #ddd; background: #fff; border-radius: 4px; cursor: pointer; font-size: 12px; color: #333; transition: all 0.2s; white-space: nowrap; display: inline-flex; align-items: center; justify-content: center; line-height: 1.5; } .btn:hover { background: #f5f5f5; border-color: #ccc; } .btn-primary { background: #667eea; color: white; border-color: #667eea; } .btn-primary:hover { background: #5a6fd6; } .btn-danger { color: #e74c3c; border-color: #e74c3c; } .btn-danger:hover { background: #e74c3c; color: white; } .search { padding: 8px 12px; border-bottom: 1px solid #eee; flex-shrink: 0; } .search input { width: 100%; padding: 6px 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; outline: none; background: #fff; color: #333; } .search input:focus { border-color: #667eea; } .cookie-list { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 8px 12px; } .cookie-item { background: #f9f9f9; border: 1px solid #eee; border-radius: 6px; margin-bottom: 8px; overflow: hidden; } .cookie-header { display: flex; align-items: center; padding: 8px 10px; cursor: pointer; user-select: none; } .cookie-header:hover { background: #f0f0f0; } .cookie-name { font-weight: 600; font-size: 13px; color: #333; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .cookie-value-preview { font-size: 11px; color: #888; max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-left: 8px; flex-shrink: 0; } .cookie-toggle { font-size: 12px; color: #999; margin-left: 8px; transition: transform 0.2s; flex-shrink: 0; } .cookie-toggle.expanded { transform: rotate(180deg); } .cookie-details { display: none; padding: 10px; background: #fff; border-top: 1px solid #eee; } .cookie-details.show { display: block; } .detail-row { display: flex; margin-bottom: 6px; font-size: 12px; align-items: flex-start; } .detail-label { width: 70px; color: #666; flex-shrink: 0; white-space: nowrap; } .detail-value { flex: 1; color: #333; word-break: break-all; min-width: 0; } .detail-actions { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; } .empty { text-align: center; color: #999; padding: 40px 20px; font-size: 14px; } .status { padding: 6px 12px; background: #f5f5f5; border-top: 1px solid #eee; font-size: 11px; color: #666; flex-shrink: 0; } /* 模态框 */ .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.4); z-index: 2147483647; display: flex; align-items: center; justify-content: center; } .modal { background: white; border-radius: 8px; width: 340px; max-width: 90%; max-height: 80vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0,0,0,0.2); } .modal-header { padding: 14px 16px; border-bottom: 1px solid #eee; font-weight: 600; font-size: 15px; color: #333; } .modal-body { padding: 16px; } .form-group { margin-bottom: 12px; } .form-group label { display: block; font-size: 12px; color: #666; margin-bottom: 4px; } .form-group input, .form-group select { width: 100%; padding: 8px 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; outline: none; background: #fff; color: #333; } .form-group input:focus, .form-group select:focus { border-color: #667eea; } .form-group input[type="checkbox"] { width: auto; margin-right: 6px; } .modal-footer { padding: 12px 16px; border-top: 1px solid #eee; display: flex; justify-content: flex-end; gap: 8px; } /* 浮动按钮 */ .fab { position: fixed; bottom: 80px; right: 16px; width: 44px; height: 44px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; border-radius: 50%; color: white; font-size: 20px; cursor: pointer; z-index: 2147483646; box-shadow: 0 2px 10px rgba(0,0,0,0.2); display: flex; align-items: center; justify-content: center; transition: transform 0.2s; } .fab:hover { transform: scale(1.1); } /* Toast */ .toast { position: fixed; bottom: 140px; right: 20px; background: #333; color: white; padding: 10px 18px; border-radius: 6px; font-size: 13px; z-index: 2147483647; opacity: 0; transform: translateY(10px); transition: all 0.3s; white-space: nowrap; } .toast.show { opacity: 1; transform: translateY(0); } /* 移动端适配 */ @media (max-width: 480px) { .panel { width: 100%; right: -100%; } .modal { width: 90%; } } `; // ==================== 工具函数 ==================== function escapeHtml(str) { if (!str) return ''; return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function formatDate(timestamp) { if (!timestamp) return '会话'; const d = new Date(timestamp * 1000); return d.toLocaleString('zh-CN'); } function getDomain() { return location.hostname; } // ==================== Cookie 操作 ==================== function isRealError(error) { if (!error) return false; if (typeof error === 'string' && error.toLowerCase() === 'success') return false; return true; } const CookieAPI = { list(callback) { GM_cookie.list({ url: location.href }, (cookies, error) => { if (isRealError(error)) { console.error('[Cookie Manager] 列出cookie失败:', error); callback([], error); return; } callback(cookies || [], null); }); }, set(details, callback) { const cookieDetails = { url: location.href, name: details.name, value: details.value, path: details.path || '/', secure: details.secure || false, httpOnly: details.httpOnly || false, sameSite: details.sameSite || 'unspecified', }; if (details.domain) cookieDetails.domain = details.domain; if (details.expirationDate) cookieDetails.expirationDate = details.expirationDate; GM_cookie.set(cookieDetails, (error) => { if (isRealError(error)) { console.error('[Cookie Manager] 设置cookie失败:', error); callback(error); return; } callback(null); }); }, delete(details, callback) { GM_cookie.delete({ url: location.href, name: details.name, storeId: details.storeId, }, (error) => { if (isRealError(error)) { console.error('[Cookie Manager] 删除cookie失败:', error); callback(error); return; } callback(null); }); }, deleteAll(cookies, callback) { let count = 0; let failCount = 0; const total = cookies.length; if (total === 0) { callback(null, 0); return; } cookies.forEach((cookie) => { CookieAPI.delete(cookie, (error) => { if (error) failCount++; count++; if (count === total) { callback(failCount > 0 ? '部分删除失败' : null, count - failCount); } }); }); }, exportJSON(cookies) { return JSON.stringify(cookies, null, 2); }, importJSON(jsonStr, callback) { try { const cookies = JSON.parse(jsonStr); if (!Array.isArray(cookies)) { callback(new Error('无效格式:需要Cookie数组')); return; } let count = 0; let failCount = 0; const total = cookies.length; cookies.forEach((cookie) => { if (!cookie.name) { count++; if (count === total) callback(failCount > 0 ? '部分导入失败' : null, count - failCount); return; } CookieAPI.set({ name: cookie.name, value: cookie.value || '', domain: cookie.domain, path: cookie.path, secure: cookie.secure, httpOnly: cookie.httpOnly, sameSite: cookie.sameSite, expirationDate: cookie.expirationDate, }, (error) => { if (error) failCount++; count++; if (count === total) callback(failCount > 0 ? '部分导入失败' : null, count - failCount); }); }); } catch (e) { callback(e); } }, }; // ==================== UI 组件(Shadow DOM)==================== class CookieManagerUI { constructor() { this.cookies = []; this.searchTerm = ''; this.expandedItems = new Set(); this.init(); } init() { // 创建 Shadow DOM 宿主 this.host = document.createElement('div'); this.host.id = 'cookie-manager-root'; this.shadow = this.host.attachShadow({ mode: 'open' }); // 注入样式到 Shadow DOM const styleEl = document.createElement('style'); styleEl.textContent = STYLES; this.shadow.appendChild(styleEl); // 创建浮动按钮 this.createFAB(); // 创建面板 this.createPanel(); // 挂载到页面 document.body.appendChild(this.host); // 注册菜单命令 this.registerMenuCommands(); // 监听快捷键 this.registerShortcuts(); } // 在 Shadow DOM 中查找元素 $(selector) { return this.shadow.querySelector(selector); } $$(selector) { return this.shadow.querySelectorAll(selector); } showToast(msg, duration = 2000) { let toast = this.$('.toast'); if (!toast) { toast = document.createElement('div'); toast.className = 'toast'; this.shadow.appendChild(toast); } toast.textContent = msg; toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), duration); } createFAB() { this.fab = document.createElement('button'); this.fab.className = 'fab'; this.fab.innerHTML = '🍪'; this.fab.addEventListener('click', () => this.togglePanel()); this.shadow.appendChild(this.fab); } createPanel() { this.panel = document.createElement('div'); this.panel.className = 'panel'; this.panel.innerHTML = `