您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Popup to show who unfollowed you on Character.AI!
// ==UserScript== // @name Character.AI Follower Tracker // @namespace http://tampermonkey.net/ // @icon https://www.google.com/s2/favicons?sz=64&domain=character.ai // @version 1.8 // @description Popup to show who unfollowed you on Character.AI! // @author Kio + Claude + Gemini 💗 // @match https://character.ai/* // @match *://character.ai/*// // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; // --- 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: 10002; 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: 10001; 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: 20001; 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-progress-bar { width: 100%; height: 8px; background: #e9d5ff; border-radius: 4px; margin-top: 10px; overflow: hidden; } .cai-progress-fill { height: 100%; background: linear-gradient(90deg, #c4b5fd, #a78bfa); transition: width 0.3s ease; border-radius: 4px; } `; document.head.appendChild(document.createElement('style')).innerHTML = styleSheet; const APP_PREFIX = 'cai_tracker_v15_'; let captureMode = null; let panelVisible = false; let isCapturing = false; let captureProgress = 0; let preparedMode = null; let modalObserver = null; // --- Toast Notification Function --- function showToast(message, type = 'success') { 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 SCROLL & CAPTURE LOGIC --- function prepareForCapture(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'); // Start watching for modal dialogs startModalWatcher(); // Auto-reset after 60 seconds to prevent confusion 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(); } // --- MODAL WATCHER FUNCTIONS --- function startModalWatcher() { stopModalWatcher(); // Clean up any existing watcher modalObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === 1) { // Element node // Check if this is a modal dialog const dialog = node.querySelector ? node.querySelector('div[role="dialog"]') : null; const isDialog = node.getAttribute && node.getAttribute('role') === 'dialog'; if (dialog || isDialog) { console.log('[CAI Tracker] Modal detected, starting capture in 2 seconds...'); setTimeout(() => { if (preparedMode) { const currentMode = preparedMode; resetPreparedMode(); // Reset first to prevent multiple captures captureAllUsers(currentMode); } }, 2000); // Wait 2 seconds for modal to fully load } } }); }); }); modalObserver.observe(document.body, { childList: true, subtree: true }); console.log('[CAI Tracker] Modal watcher started'); } function stopModalWatcher() { if (modalObserver) { modalObserver.disconnect(); modalObserver = null; console.log('[CAI Tracker] Modal watcher stopped'); } } async function captureAllUsers(listType) { if (isCapturing) { showToast("Already capturing, please wait...", 'info'); return; } isCapturing = true; const users = new Map(); let lastCount = 0; let noChangeCount = 0; let scrollAttempts = 0; const maxScrollAttempts = 100; const maxNoChangeAttempts = 5; 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 { // Find the dialog/modal let dialog = document.querySelector('div[role="dialog"]'); if (!dialog) { dialog = document.querySelector('div[data-testid="modal"]'); } if (!dialog) { dialog = document.querySelector('.modal, [class*="modal"], [class*="dialog"], [class*="overlay"]'); } if (!dialog) { throw new Error("No dialog found. Make sure the followers/following list is open."); } // Find scrollable container let scrollableContainer = dialog.querySelector('[class*="scroll"], [class*="overflow"], [style*="overflow"]'); if (!scrollableContainer) { // Look for container with many child elements (likely the list) const containers = dialog.querySelectorAll('div'); for (const container of containers) { if (container.children.length > 10) { scrollableContainer = container; break; } } } if (!scrollableContainer) { scrollableContainer = dialog; } console.log('[CAI Tracker] Starting enhanced capture for:', listType); console.log('[CAI Tracker] Dialog:', dialog); console.log('[CAI Tracker] Scrollable container:', scrollableContainer); // Capture loop with auto-scrolling while (scrollAttempts < maxScrollAttempts && noChangeCount < maxNoChangeAttempts) { // Capture current users const currentUsers = captureUsersFromDOM(scrollableContainer); // Add new users to our collection 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`); // Update UI if (statusBtn) { statusBtn.textContent = `🔄 Capturing ${listType}... ${currentCount} found`; } if (statusEl) { statusEl.textContent = `${listType}: Capturing... ${currentCount} found`; } // Check if we found new users if (currentCount === lastCount) { noChangeCount++; } else { noChangeCount = 0; lastCount = currentCount; } // Scroll down to load more users scrollableContainer.scrollTop = scrollableContainer.scrollHeight; // Also try scrolling the dialog itself dialog.scrollTop = dialog.scrollHeight; // Wait for new content to load await new Promise(resolve => setTimeout(resolve, 1500)); scrollAttempts++; // Break if we haven't found new users in several attempts if (noChangeCount >= maxNoChangeAttempts) { console.log('[CAI Tracker] No new users found in', maxNoChangeAttempts, 'attempts. Stopping.'); break; } } const finalUsers = Array.from(users.values()); console.log(`[CAI Tracker] Final capture complete: ${finalUsers.length} users`); if (finalUsers.length === 0) { throw new Error("No users found. The page structure might have changed."); } // Save to localStorage 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; // Reset UI if (statusBtn) { statusBtn.classList.remove('capturing'); resetPreparedMode(); // This will reset the button text } updateStatus(); } } function captureUsersFromDOM(container) { const users = new Map(); const processedElements = new Set(); // Enhanced selectors for Character.AI const userContainerSelectors = [ 'a[href*="/profile/"]', 'a[href*="/user/"]', 'a[href*="@"]', 'div[class*="user"]', 'div[class*="profile"]', 'div[class*="member"]', 'div[class*="follow"]', 'div[class*="avatar"]', '[data-testid*="user"]', '[data-testid*="profile"]', 'div[role="button"]', 'button', // More generic selectors 'div > div > div', // Common nested structure 'li', 'article' ]; userContainerSelectors.forEach(selector => { 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); } }); }); // If still no users, try a more aggressive approach if (users.size === 0) { 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 to extract username from various sources const textSelectors = [ 'div[class*="text-ellipsis"]', 'span[class*="text-ellipsis"]', '.username', '[class*="username"]', '[class*="name"]', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div[class*="title"]', 'span[class*="title"]', 'p', 'span', 'div' ]; // Try each selector 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 = text; break; } } } // If no username from child elements, try the element itself if (!username && element.textContent) { const text = element.textContent.trim(); if (isValidUsername(text)) { username = text; } } // Extract avatar const img = element.querySelector('img'); if (img && img.src && !img.src.includes('data:image')) { avatar = img.src; } // Clean up username if (username) { username = cleanUsername(username); } // Validate final username if (username && isValidUsername(username)) { return { username, avatar: avatar || 'https://placehold.co/40x40/f5f3ff/6d28d9?text=?' }; } return null; } function isValidUsername(text) { if (!text || typeof text !== 'string') return false; // Clean the text first const cleaned = text.trim(); // Check length if (cleaned.length === 0 || cleaned.length > 50) return false; // Exclude common UI text 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; } // Exclude pure numbers or numbers with common suffixes if (/^\d+$/.test(cleaned) || /^\d+\s*(followers?|following|posts?|likes?)$/i.test(cleaned)) { return false; } // Exclude text with line breaks if (cleaned.includes('\n')) return false; // Must have some alphanumeric characters if (!/[a-zA-Z0-9]/.test(cleaned)) return false; return true; } function cleanUsername(username) { if (!username) return ''; // Take only the first line let cleaned = username.split('\n')[0].trim(); // Remove leading numbers (like "1. username") cleaned = cleaned.replace(/^\d+\s*[.)\-]\s*/, ''); // Remove trailing numbers in parentheses (like "username (123)") cleaned = cleaned.replace(/\s*\(\d+\)\s*$/, ''); // Normalize whitespace cleaned = cleaned.replace(/\s+/g, ' ').trim(); return cleaned; } // --- UI FUNCTIONS --- function createToggleButton() { const existingBtn = document.getElementById('cai-toggle-btn'); if (existingBtn) return; 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() { if (panelVisible) { hidePanel(); } else { showPanel(); } }); document.body.appendChild(toggleBtn); } function showPanel() { 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! (Scroll to the bottom of the selected list, it might not capture banned/deleted users)</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); // Add event listeners 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() { const panel = document.getElementById('cai-main-panel'); if (panel) { panel.remove(); } panelVisible = false; captureMode = null; resetPreparedMode(); // Clean up any prepared state } 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() { setTimeout(() => { createToggleButton(); }, 1000); } // 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')) { createToggleButton(); } }); if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址