您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Popup to show who unfollowed you on Character.AI! (Updated for new design)
// ==UserScript== // @name Character.AI Follower Tracker (Updated) // @namespace http://tampermonkey.net/ // @icon https://www.google.com/s2/favicons?sz=64&domain=character.ai // @version 2.0 // @description Popup to show who unfollowed you on Character.AI! (Updated for new design) // @author Kio + Claude + Gemini 💗 // @match https://character.ai/* // @match *://character.ai/* // @match https://character.ai/ // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; console.log('[CAI Tracker v2.0] Script loaded'); // --- S T Y L E S --- const styleSheet = ` @import url('https://fonts.googleapis.com/css2?family=Ubuntu:wght@400;700&display=swap'); .cai-tracker-panel, .cai-results-popup { position: fixed; z-index: 99999 !important; background: #f5f3ff; color: #6d28d9; border: 2px solid #ddd6fe; border-radius: 20px; padding: 25px; box-shadow: 0 10px 50px rgba(196, 181, 253, 0.5); font-family: 'Ubuntu', sans-serif; transition: transform 0.3s ease-in-out; } .cai-tracker-panel:hover, .cai-results-popup:hover { transform: translateY(-5px); } .cai-tracker-panel { bottom: 20px; right: 20px; width: 380px; text-align: center; } .cai-tracker-panel h2 { font-weight: 700; color: #8b5cf6; margin: 0 0 10px 0; font-size: 1.5rem; } .cai-tracker-panel p { font-size: 0.9rem; line-height: 1.4; color: #7c3aed; margin: 0 0 15px 0; } .cai-manual-button { background-image: linear-gradient(135deg, #e4d4f7, #d1c2f0); color: #5b21b6; border: none; padding: 12px 18px; width: 100%; border-radius: 12px; cursor: pointer; font-size: 1rem; font-weight: 700; transition: all 0.3s ease; margin-top: 10px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); } .cai-manual-button:hover { transform: scale(1.03); background-image: linear-gradient(135deg, #d1c2f0, #c4b5fd); } .cai-manual-button.ready { background-image: linear-gradient(135deg, #34d399, #22c55e); color: white; } .cai-manual-button.capturing { background-image: linear-gradient(135deg, #fbbf24, #f59e0b); color: white; animation: pulse 2s infinite; } .cai-manual-button:disabled { background-image: none; background-color: #e9d5ff; cursor: not-allowed; opacity: 0.7; color: #9ca3af; } @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.7; } 100% { opacity: 1; } } .cai-status-text { margin-top: 15px; font-size: 0.9rem; font-weight: 700; } .cai-status-ok { color: #22c55e; } .cai-status-not-ok { color: #ef4444; } .cai-status-capturing { color: #f59e0b; } .cai-tracker-close-btn { position: absolute; top: 10px; right: 15px; background: none; border: none; color: #c4b5fd; font-size: 1.8rem; cursor: pointer; transition: transform 0.2s; } .cai-tracker-close-btn:hover { transform: scale(1.2); } .cai-tracker-toggle-btn { position: fixed; bottom: 20px; right: 20px; z-index: 99998 !important; background: linear-gradient(135deg, #e4d4f7, #d1c2f0, #c4b5fd); border: none; border-radius: 50%; width: 60px; height: 60px; cursor: pointer; font-size: 2rem; color: #5b21b6; box-shadow: 0 8px 25px rgba(139, 92, 246, 0.4); transition: all 0.3s ease; } .cai-tracker-toggle-btn:hover { transform: scale(1.1); box-shadow: 0 12px 35px rgba(139, 92, 246, 0.6); background: linear-gradient(135deg, #d1c2f0, #c4b5fd, #a78bfa); } .cai-results-popup { top: 10%; right: 20px; max-height: 80vh; overflow-y: auto; width: 320px; background: linear-gradient(135deg, #f5f3ff, #ede9fe); background-size: 200% 200%; animation: pan-background 10s ease infinite; } @keyframes pan-background { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } .cai-results-row { display: flex; align-items: center; margin-bottom: 8px; padding: 8px; border-radius: 10px; transition: background-color 0.2s; } .cai-results-row:hover { background-color: rgba(255,255,255,0.5); } .cai-results-row img { width: 40px; height: 40px; border-radius: 50%; margin-right: 12px; border: 2px solid #ddd6fe; } .cai-results-row span { color: #5b21b6; font-weight: 700; } .cai-toast-notification { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 12px 25px; border-radius: 50px; font-family: 'Ubuntu', sans-serif; font-size: 1rem; font-weight: 700; color: white; z-index: 99999 !important; box-shadow: 0 5px 15px rgba(0,0,0,0.2); opacity: 0; transition: opacity 0.4s ease, top 0.4s ease; } .cai-toast-notification.success { background-image: linear-gradient(to right, #34d399, #22c55e); } .cai-toast-notification.error { background-image: linear-gradient(to right, #f87171, #ef4444); } .cai-toast-notification.info { background-image: linear-gradient(to right, #60a5fa, #3b82f6); } .cai-toast-notification.show { top: 40px; opacity: 1; } .cai-debug-info { margin-top: 15px; padding: 10px; background: rgba(139, 92, 246, 0.1); border-radius: 8px; font-size: 0.75rem; color: #7c3aed; max-height: 100px; overflow-y: auto; } `; document.head.appendChild(document.createElement('style')).innerHTML = styleSheet; const APP_PREFIX = 'cai_tracker_v20_'; let captureMode = null; let panelVisible = false; let isCapturing = false; let preparedMode = null; let modalObserver = null; // --- Toast Notification Function --- function showToast(message, type = 'success') { console.log(`[CAI Tracker] Toast: ${type} - ${message}`); const toast = document.createElement('div'); toast.className = `cai-toast-notification ${type}`; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => toast.classList.add('show'), 10); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 500); }, 3000); } // --- ENHANCED MODAL DETECTION --- function startModalWatcher() { stopModalWatcher(); console.log('[CAI Tracker] Starting modal watcher...'); modalObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === 1) { // Multiple detection strategies for new C.AI design const isModal = node.querySelector?.('div[role="dialog"]') || node.getAttribute?.('role') === 'dialog' || node.querySelector?.('[role="dialog"]') || node.querySelector?.('div[class*="modal"]') || node.querySelector?.('div[class*="Modal"]') || node.querySelector?.('div[class*="dialog"]') || node.querySelector?.('div[class*="Dialog"]') || node.querySelector?.('div[class*="overlay"]') || node.querySelector?.('div[class*="Overlay"]') || (node.className && typeof node.className === 'string' && (node.className.includes('modal') || node.className.includes('Modal') || node.className.includes('dialog') || node.className.includes('Dialog'))); if (isModal) { console.log('[CAI Tracker] ✅ Modal detected!', node); setTimeout(() => { if (preparedMode) { console.log('[CAI Tracker] Starting capture for:', preparedMode); const currentMode = preparedMode; resetPreparedMode(); captureAllUsers(currentMode); } }, 2500); } } }); }); }); modalObserver.observe(document.body, { childList: true, subtree: true }); console.log('[CAI Tracker] Modal watcher active'); } function stopModalWatcher() { if (modalObserver) { modalObserver.disconnect(); modalObserver = null; console.log('[CAI Tracker] Modal watcher stopped'); } } function prepareForCapture(type) { console.log('[CAI Tracker] Preparing for capture:', type); preparedMode = type; const btn = document.getElementById(`prepare-${type}-btn`); const instructionEl = document.getElementById('instruction-text'); const instructionContent = document.getElementById('instruction-content'); if (btn) { btn.classList.add('ready'); btn.textContent = `✅ Ready! Now open your ${type} list`; } if (instructionEl && instructionContent) { instructionContent.textContent = `Now click on "${type}" in someone's profile to open the list. Capture will start automatically!`; instructionEl.style.display = 'block'; } showToast(`Ready to capture ${type}! Now open the ${type} list.`, 'success'); startModalWatcher(); setTimeout(() => { if (preparedMode === type) { resetPreparedMode(); showToast(`Preparation timeout. Click "Prepare" again if needed.`, 'info'); } }, 60000); } function resetPreparedMode() { const oldMode = preparedMode; preparedMode = null; if (oldMode) { const btn = document.getElementById(`prepare-${oldMode}-btn`); if (btn) { btn.classList.remove('ready'); btn.textContent = `${oldMode === 'followers' ? '1' : '2'}. Prepare for ${oldMode.charAt(0).toUpperCase() + oldMode.slice(1)}`; } } const instructionEl = document.getElementById('instruction-text'); if (instructionEl) { instructionEl.style.display = 'none'; } stopModalWatcher(); } // --- ENHANCED CAPTURE LOGIC --- async function captureAllUsers(listType) { if (isCapturing) { showToast("Already capturing, please wait...", 'info'); return; } console.log('[CAI Tracker] Starting capture for:', listType); isCapturing = true; const users = new Map(); let lastCount = 0; let noChangeCount = 0; let scrollAttempts = 0; const maxScrollAttempts = 150; const maxNoChangeAttempts = 6; const statusBtn = document.getElementById(`prepare-${listType}-btn`); const statusEl = document.getElementById(`${listType}-status`); if (statusBtn) { statusBtn.classList.add('capturing'); statusBtn.textContent = `🔄 Capturing ${listType}... 0 found`; } if (statusEl) { statusEl.className = 'cai-status-capturing'; statusEl.textContent = `${listType}: Capturing... 0 found`; } try { // Enhanced dialog detection for new C.AI design await new Promise(resolve => setTimeout(resolve, 1000)); let dialog = document.querySelector('div[role="dialog"]'); if (!dialog) { const allDialogs = document.querySelectorAll('[role="dialog"], [class*="modal"], [class*="Modal"], [class*="dialog"], [class*="Dialog"], [class*="overlay"], [class*="Overlay"]'); if (allDialogs.length > 0) { dialog = allDialogs[allDialogs.length - 1]; } } if (!dialog) { const allDivs = document.querySelectorAll('div'); for (const div of allDivs) { const style = window.getComputedStyle(div); if (style.position === 'fixed' && parseInt(style.zIndex) > 1000 && div.children.length > 0) { dialog = div; break; } } } if (!dialog) { throw new Error("No dialog found. Make sure the followers/following list is open."); } console.log('[CAI Tracker] Dialog found:', dialog); // Find scrollable container with improved detection let scrollableContainer = findScrollableContainer(dialog); console.log('[CAI Tracker] Scrollable container:', scrollableContainer); console.log('[CAI Tracker] Starting capture loop...'); // Capture loop with auto-scrolling while (scrollAttempts < maxScrollAttempts && noChangeCount < maxNoChangeAttempts) { const currentUsers = captureUsersFromDOM(scrollableContainer); currentUsers.forEach(user => { if (!users.has(user.username)) { users.set(user.username, user); } }); const currentCount = users.size; console.log(`[CAI Tracker] Attempt ${scrollAttempts + 1}: Found ${currentCount} users (${currentUsers.length} new)`); if (statusBtn) { statusBtn.textContent = `🔄 Capturing ${listType}... ${currentCount} found`; } if (statusEl) { statusEl.textContent = `${listType}: Capturing... ${currentCount} found`; } if (currentCount === lastCount) { noChangeCount++; } else { noChangeCount = 0; lastCount = currentCount; } // Enhanced scrolling scrollableContainer.scrollTop = scrollableContainer.scrollHeight; dialog.scrollTop = dialog.scrollHeight; window.scrollTo(0, document.body.scrollHeight); await new Promise(resolve => setTimeout(resolve, 1800)); scrollAttempts++; if (noChangeCount >= maxNoChangeAttempts) { console.log('[CAI Tracker] No new users found, stopping.'); break; } } const finalUsers = Array.from(users.values()); console.log(`[CAI Tracker] ✅ Capture complete: ${finalUsers.length} users`); if (finalUsers.length === 0) { throw new Error("No users found. The page structure might have changed."); } localStorage.setItem(APP_PREFIX + listType, JSON.stringify(finalUsers)); showToast(`Success! Captured ${finalUsers.length} ${listType}.`, 'success'); return finalUsers; } catch (error) { console.error('[CAI Tracker] ❌ Capture error:', error); showToast(`Error: ${error.message}`, 'error'); return null; } finally { isCapturing = false; if (statusBtn) { statusBtn.classList.remove('capturing'); resetPreparedMode(); } updateStatus(); } } function findScrollableContainer(dialog) { // Strategy 1: Look for explicit scroll containers const scrollContainers = dialog.querySelectorAll('[class*="scroll"], [class*="Scroll"], [style*="overflow"]'); for (const container of scrollContainers) { const style = window.getComputedStyle(container); if (style.overflowY === 'auto' || style.overflowY === 'scroll') { if (container.scrollHeight > container.clientHeight) { return container; } } } // Strategy 2: Look for containers with many children const allContainers = dialog.querySelectorAll('div'); let bestContainer = dialog; let maxChildren = 0; for (const container of allContainers) { const childCount = container.children.length; if (childCount > maxChildren && childCount > 5) { const style = window.getComputedStyle(container); if (style.display !== 'none') { maxChildren = childCount; bestContainer = container; } } } if (bestContainer !== dialog) { return bestContainer; } // Strategy 3: Return dialog itself return dialog; } function captureUsersFromDOM(container) { const users = new Map(); const processedElements = new Set(); // Enhanced selectors for new C.AI design const selectors = [ 'a[href*="/profile/"]', 'a[href*="/public-profile/"]', 'a[href*="/@"]', 'div[class*="user"]', 'div[class*="User"]', 'div[class*="profile"]', 'div[class*="Profile"]', 'div[class*="member"]', 'div[class*="Member"]', 'div[class*="avatar"]', 'div[class*="Avatar"]', '[data-testid*="user"]', '[data-testid*="profile"]', 'li', 'article', 'div > div > a', 'div > a > div' ]; selectors.forEach(selector => { try { const elements = container.querySelectorAll(selector); elements.forEach(element => { if (processedElements.has(element)) return; processedElements.add(element); const userData = extractUserData(element); if (userData && userData.username && !users.has(userData.username)) { users.set(userData.username, userData); } }); } catch (e) { console.warn('[CAI Tracker] Selector error:', selector, e); } }); // Fallback: aggressive scan if (users.size < 5) { const allElements = container.querySelectorAll('*'); allElements.forEach(element => { if (processedElements.has(element)) return; const userData = extractUserData(element); if (userData && userData.username && !users.has(userData.username)) { users.set(userData.username, userData); processedElements.add(element); } }); } return Array.from(users.values()); } function extractUserData(element) { let username = ''; let avatar = ''; // Try href first (most reliable for new design) const href = element.href || element.querySelector('a')?.href; if (href) { const match = href.match(/\/(profile|public-profile)\/([^/?]+)|@([^/?]+)/); if (match) { username = match[2] || match[3]; } } // Try text extraction if (!username) { const textSelectors = [ '[class*="text"]', '[class*="Text"]', '[class*="name"]', '[class*="Name"]', '[class*="username"]', '[class*="Username"]', 'span', 'div', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' ]; for (const selector of textSelectors) { const textEl = element.querySelector(selector); if (textEl && textEl.textContent && textEl.textContent.trim()) { const text = textEl.textContent.trim(); if (isValidUsername(text)) { username = cleanUsername(text); break; } } } } // If still no username, try element's own text if (!username && element.textContent) { const text = element.textContent.trim(); if (isValidUsername(text)) { username = cleanUsername(text); } } // Extract avatar const img = element.querySelector('img'); if (img && img.src && !img.src.includes('data:image')) { avatar = img.src; } if (username && isValidUsername(username)) { return { username: username, avatar: avatar || 'https://placehold.co/40x40/f5f3ff/6d28d9?text=?' }; } return null; } function isValidUsername(text) { if (!text || typeof text !== 'string') return false; const cleaned = text.trim(); if (cleaned.length === 0 || cleaned.length > 50) return false; const excludePatterns = [ 'follow', 'following', 'followers', 'message', 'block', 'report', 'back', 'close', 'cancel', 'ok', 'yes', 'no', 'save', 'edit', 'delete', 'remove', 'add', 'create', 'new', 'search', 'filter', 'sort', 'view', 'show', 'hide', 'more', 'less', 'next', 'previous', 'loading', 'error', 'success', 'warning', 'info', 'help', 'about', 'settings', 'profile', 'account', 'logout', 'login', 'sign', 'register', 'submit', 'send', 'receive', 'inbox', 'notifications' ]; const lowerText = cleaned.toLowerCase(); if (excludePatterns.some(pattern => lowerText.includes(pattern))) { return false; } if (/^\d+$/.test(cleaned) || /^\d+\s*(followers?|following|posts?|likes?)$/i.test(cleaned)) { return false; } if (cleaned.includes('\n')) return false; if (!/[a-zA-Z0-9]/.test(cleaned)) return false; return true; } function cleanUsername(username) { if (!username) return ''; let cleaned = username.split('\n')[0].trim(); cleaned = cleaned.replace(/^\d+\s*[.)\-]\s*/, ''); cleaned = cleaned.replace(/\s*\(\d+\)\s*$/, ''); cleaned = cleaned.replace(/\s+/g, ' ').trim(); return cleaned; } // --- UI FUNCTIONS --- function createToggleButton() { const existingBtn = document.getElementById('cai-toggle-btn'); if (existingBtn) { console.log('[CAI Tracker] Toggle button already exists'); return; } console.log('[CAI Tracker] Creating toggle button...'); const toggleBtn = document.createElement('button'); toggleBtn.id = 'cai-toggle-btn'; toggleBtn.className = 'cai-tracker-toggle-btn'; toggleBtn.innerHTML = '🔍'; toggleBtn.title = 'Toggle Follower Tracker'; toggleBtn.addEventListener('click', function() { console.log('[CAI Tracker] Toggle button clicked'); if (panelVisible) { hidePanel(); } else { showPanel(); } }); document.body.appendChild(toggleBtn); console.log('[CAI Tracker] ✅ Toggle button created successfully'); showToast('Follower Tracker loaded! Click the 🔍 button to start.', 'info'); } function showPanel() { console.log('[CAI Tracker] Showing panel...'); const existingPanel = document.getElementById('cai-main-panel'); if (existingPanel) { existingPanel.remove(); } const panel = document.createElement('div'); panel.id = 'cai-main-panel'; panel.className = 'cai-tracker-panel'; panel.innerHTML = ` <button class="cai-tracker-close-btn">×</button> <h2>Follower Balance Tool</h2> <p><b>How to:</b> Click "Prepare" first, then open the followers/following list. The capture will start automatically when the list opens!</p> <button id="prepare-followers-btn" class="cai-manual-button">1. Prepare for Followers</button> <button id="prepare-following-btn" class="cai-manual-button">2. Prepare for Following</button> <div id="instruction-text" style="margin-top: 10px; font-size: 0.8rem; color: #7c3aed; display: none;"> 📋 <span id="instruction-content"></span> </div> <div id="status-container" class="cai-status-text"> <span id="followers-status" class="cai-status-not-ok">Followers: Not captured</span><br> <span id="following-status" class="cai-status-not-ok">Following: Not captured</span> </div> <button id="compare-btn" class="cai-manual-button" disabled>3. Compare & Show Results</button> `; document.body.appendChild(panel); panel.querySelector('.cai-tracker-close-btn').addEventListener('click', hidePanel); document.getElementById('prepare-followers-btn').addEventListener('click', function() { if (isCapturing) return; prepareForCapture('followers'); }); document.getElementById('prepare-following-btn').addEventListener('click', function() { if (isCapturing) return; prepareForCapture('following'); }); document.getElementById('compare-btn').addEventListener('click', showResults); panelVisible = true; updateStatus(); } function hidePanel() { console.log('[CAI Tracker] Hiding panel...'); const panel = document.getElementById('cai-main-panel'); if (panel) { panel.remove(); } panelVisible = false; captureMode = null; resetPreparedMode(); } function updateStatus() { const followers = JSON.parse(localStorage.getItem(APP_PREFIX + 'followers') || '[]'); const following = JSON.parse(localStorage.getItem(APP_PREFIX + 'following') || '[]'); const followersStatusEl = document.getElementById('followers-status'); const followingStatusEl = document.getElementById('following-status'); const compareBtn = document.getElementById('compare-btn'); if (followersStatusEl && !followersStatusEl.className.includes('capturing')) { if (followers.length > 0) { followersStatusEl.textContent = `Followers: ${followers.length} captured`; followersStatusEl.className = 'cai-status-ok'; } else { followersStatusEl.textContent = `Followers: Not captured`; followersStatusEl.className = 'cai-status-not-ok'; } } if (followingStatusEl && !followingStatusEl.className.includes('capturing')) { if (following.length > 0) { followingStatusEl.textContent = `Following: ${following.length} captured`; followingStatusEl.className = 'cai-status-ok'; } else { followingStatusEl.textContent = `Following: Not captured`; followingStatusEl.className = 'cai-status-not-ok'; } } if (compareBtn) { compareBtn.disabled = !(followers.length > 0 && following.length > 0) || isCapturing; } } function showResults() { const followingList = JSON.parse(localStorage.getItem(APP_PREFIX + 'following') || "[]"); const followersList = JSON.parse(localStorage.getItem(APP_PREFIX + 'followers') || "[]"); if (followingList.length === 0 || followersList.length === 0) { showToast("Please capture both lists before comparing.", 'error'); return; } const followerUsernames = new Set(followersList.map(u => u.username.toLowerCase())); const notFollowingBack = followingList.filter(u => !followerUsernames.has(u.username.toLowerCase())); const existing = document.querySelector('.cai-results-popup'); if (existing) existing.remove(); const wrapper = document.createElement("div"); wrapper.className = 'cai-results-popup'; let userListHTML = notFollowingBack.map(user => ` <div class="cai-results-row"> <img src="${user.avatar}" alt="${user.username}'s avatar" onerror="this.src='https://placehold.co/40x40/f5f3ff/6d28d9?text=?'"> <span>${user.username}</span> </div> `).join(''); if (notFollowingBack.length === 0) { userListHTML = "<p style='color: #581c87; text-align: center; padding: 20px;'>Everyone you follow follows you back!</p>"; } wrapper.innerHTML = ` <button class="cai-tracker-close-btn">×</button> <h3>Doesn't Follow Back (${notFollowingBack.length})</h3> <p style="font-size: 0.8rem; color: #7c3aed; margin-bottom: 15px;"> 📊 Followers: ${followersList.length} | Following: ${followingList.length} </p> ${userListHTML} `; document.body.appendChild(wrapper); wrapper.querySelector('.cai-tracker-close-btn').addEventListener('click', () => wrapper.remove()); } function init() { console.log('[CAI Tracker] Initializing...'); setTimeout(() => { console.log('[CAI Tracker] Creating toggle button (delayed)...'); createToggleButton(); }, 3000); // Increased delay for new site } // Initialize if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // Backup observer const observer = new MutationObserver(() => { if (document.body && !document.getElementById('cai-toggle-btn')) { console.log('[CAI Tracker] Toggle button missing, recreating...'); createToggleButton(); } }); if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } })();