您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add an Agnai import button to character pages on characterhub.org and chub.ai
当前为
// ==UserScript== // @name Chub Agnai Import Button // @namespace http://tampermonkey.net/ // @version 1.22 // @description Add an Agnai import button to character pages on characterhub.org and chub.ai // @author EliseWindbloom // @match https://www.characterhub.org/* // @match https://characterhub.org/* // @match https://chub.ai/* // @match https://*.chub.ai/* // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; // Config const DEBUG = false; // Set to true to enable debug logging const CHECK_INTERVAL = 2000; // Interval for periodic checks in ms // Debug logging function function log(message) { if (DEBUG) { console.log(`[Agnai Import Button] ${message}`); } } // Check if we're on a character page function isCharacterPage() { return window.location.pathname.includes('/characters/'); } // Determine which site we're on function getSiteType() { const hostname = window.location.hostname; if (hostname.includes('characterhub.org')) { return 'characterhub'; } else if (hostname.includes('chub.ai')) { return 'chub'; } return 'unknown'; } // Extract the character identifier from the URL function getCharacterIdentifier() { const path = window.location.pathname; if (path.includes('/characters/')) { return path.split('characters/')[1]; } return null; } // Check if we're on a numeric ID page (like https://chub.ai/characters/2535134) function isNumericIdPage() { const identifier = getCharacterIdentifier(); return identifier && /^\d+$/.test(identifier); } // Extract full character identifier from image source - with safety checks function extractIdentifierFromImage() { try { // For chub.ai, look for the character image const charImage = document.querySelector('.ant-image-img'); if (charImage && charImage.src) { const match = charImage.src.match(/avatars\/(.*?)\/chara_card/); if (match && match[1]) { log(`Extracted identifier from image: ${match[1]}`); return match[1]; } } } catch (err) { console.error("[Agnai Import Button] Error extracting identifier from image:", err); } return null; } // Get the proper identifier, handling numeric ID pages function getProperIdentifier() { if (getSiteType() === 'chub' && isNumericIdPage()) { // For numeric IDs, we need to extract from the image const imageId = extractIdentifierFromImage(); if (imageId) return imageId; } // Otherwise or as fallback use the URL path return getCharacterIdentifier(); } // Construct the Agnai import URL function getImportUrl(identifier) { if (!identifier) return null; return `https://agnai.chat/character/create?import=${encodeURIComponent(identifier)}`; } // Function to create the button element for CharacterHub function createCharacterHubButton(importUrl) { if (!importUrl) return null; // Create the tooltip wrapper const tooltipDiv = document.createElement('div'); tooltipDiv.className = 'tooltip'; // Add the tooltip text const tooltipText = document.createElement('div'); tooltipText.className = 'tooltiptext text-xs'; tooltipText.textContent = 'Import to Agnai'; tooltipDiv.appendChild(tooltipText); // Create the button (as an <a> tag) const button = document.createElement('a'); button.href = importUrl; button.target = '_blank'; button.rel = 'noopener noreferrer'; button.className = 'primary-button mr-1 max-w-xs max-h-sm inline-flex justify-center rounded-md px-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 py-1 agnai-import-button'; button.style.cssText = 'display: inline-flex !important; visibility: visible !important; opacity: 1 !important; position: relative !important; z-index: 9999 !important; background: #0066cc !important; transition: background 0.2s, transform 0.2s !important;'; // Add hover effects button.addEventListener('mouseover', () => { button.style.background = '#004d99 !important'; // Darker blue on hover button.style.transform = 'scale(1.05)'; button.style.cursor = 'pointer'; }); button.addEventListener('mouseout', () => { button.style.background = '#0066cc !important'; // Original darker blue button.style.transform = 'scale(1)'; }); // Add the icon const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('class', 's16 icon'); const image = document.createElementNS('http://www.w3.org/2000/svg', 'image'); image.setAttribute('href', '/assets/download_icon-41be3d17.svg'); svg.appendChild(image); button.appendChild(svg); // Add the "Ag" text, bigger and bolder const textDiv = document.createElement('div'); textDiv.className = '-mt-0.5 s18 ml-2'; textDiv.textContent = 'Ag'; button.appendChild(textDiv); // Append the button to the tooltip wrapper tooltipDiv.appendChild(button); return tooltipDiv; } // Function to create the button element for Chub.ai function createChubButton(importUrl) { if (!importUrl) return null; // Create the button (using ant design to match the site's style) const button = document.createElement('button'); button.type = 'button'; button.className = 'ant-btn css-ug5vhh ant-btn-default ant-btn-block mt-2 agnai-import-button'; button.setAttribute('data-import-url', importUrl); // Set initial styles button.style.cssText = ` background-color: #0066cc !important; color: white !important; border-color: #0066cc !important; display: flex !important; justify-content: center !important; align-items: center !important; margin-top: 8px !important; transition: all 0.3s ease !important; cursor: pointer !important; `; // Add hover effects with inline styles to ensure they take precedence button.addEventListener('mouseover', () => { button.style.backgroundColor = '#004d99 !important'; button.style.borderColor = '#004d99 !important'; button.style.transform = 'scale(1.03)'; button.style.boxShadow = '0 2px 8px rgba(0, 77, 153, 0.3)'; }); button.addEventListener('mouseout', () => { button.style.backgroundColor = '#0066cc !important'; button.style.borderColor = '#0066cc !important'; button.style.transform = 'scale(1)'; button.style.boxShadow = 'none'; }); // Create onclick event to open in new tab button.addEventListener('click', (e) => { e.preventDefault(); window.open(importUrl, '_blank'); }); // Create content with icon and text const span = document.createElement('span'); span.textContent = 'Import to Agnai'; span.style.marginLeft = '8px'; // Create "Ag" logo/text const logoSpan = document.createElement('span'); logoSpan.textContent = 'Ag'; logoSpan.style.fontWeight = 'bold'; logoSpan.style.fontSize = '16px'; // Add elements to button button.appendChild(logoSpan); button.appendChild(span); return button; } // Function to add or re-add the button for CharacterHub function ensureCharacterHubButtonExists() { try { const container = document.querySelector('.download-buttons'); if (!container) { log('CharacterHub container not found yet'); return false; } // Check if button already exists const existingButton = container.querySelector('.agnai-import-button'); if (existingButton) { log('CharacterHub button already exists'); return true; } // Create new button const identifier = getProperIdentifier(); if (!identifier) { log('No character identifier found'); return false; } const importUrl = getImportUrl(identifier); log(`Import URL: ${importUrl}`); const button = createCharacterHubButton(importUrl); if (button) { container.appendChild(button); log('CharacterHub button added'); return true; } } catch (err) { console.error("[Agnai Import Button] Error adding CharacterHub button:", err); } return false; } // Function to add or re-add the button for Chub.ai function ensureChubButtonExists() { try { // Find the container with the import-chat button let importButtonContainer = null; const containers = document.querySelectorAll('.mt-4'); for (let i = 0; i < containers.length; i++) { if (containers[i].querySelector('.import-chat')) { importButtonContainer = containers[i]; break; } } if (!importButtonContainer) { log('Chub.ai container not found yet'); return false; } // Get proper identifier const identifier = getProperIdentifier(); if (!identifier) { log('No character identifier found'); return false; } const importUrl = getImportUrl(identifier); log(`Import URL: ${importUrl}`); // Check if button exists already const existingButton = importButtonContainer.querySelector('.agnai-import-button'); if (existingButton) { // If we're on a numeric ID page, check if we need to update the button if (getSiteType() === 'chub' && isNumericIdPage()) { const currentUrl = existingButton.getAttribute('data-import-url'); // Only update if the URLs differ if (currentUrl !== importUrl) { log('Updating button with new identifier from image'); const newButton = createChubButton(importUrl); if (newButton) { importButtonContainer.replaceChild(newButton, existingButton); return true; } } } log('Chub.ai button already exists'); return true; } // Create new button const button = createChubButton(importUrl); if (button) { importButtonContainer.appendChild(button); log('Chub.ai button added'); return true; } } catch (err) { console.error("[Agnai Import Button] Error adding Chub.ai button:", err); } return false; } // Main function to ensure button exists based on site function ensureButtonExists() { try { if (!isCharacterPage()) { log('Not on a character page, skipping button addition'); return false; } const siteType = getSiteType(); log(`Site detected: ${siteType}`); if (siteType === 'characterhub') { return ensureCharacterHubButtonExists(); } else if (siteType === 'chub') { return ensureChubButtonExists(); } } catch (err) { console.error("[Agnai Import Button] Error ensuring button exists:", err); } return false; } // Set up a MutationObserver to watch for DOM changes function setupObserver() { try { const observer = new MutationObserver((mutations) => { // Only run if we're on a character page if (isCharacterPage()) { // Check for significant changes in the DOM that would warrant adding the button for (const mutation of mutations) { // Only proceed if nodes were added if (mutation.addedNodes.length > 0) { // Check if these nodes affect our target containers let shouldCheck = false; for (const node of mutation.addedNodes) { if (node.nodeType === 1) { // Element node // Look for our target containers or images if ((node.classList && (node.classList.contains('download-buttons') || node.classList.contains('mt-4') || node.classList.contains('ant-image-img'))) || node.querySelector) { shouldCheck = true; break; } } } if (shouldCheck) { log('Relevant DOM change detected, checking for button'); setTimeout(ensureButtonExists, 100); // Small delay to let DOM settle break; } } } } }); // Start observing with a more focused approach observer.observe(document.body, { childList: true, subtree: true, attributes: false, characterData: false }); log('Observer set up to monitor DOM changes'); } catch (err) { console.error("[Agnai Import Button] Error setting up observer:", err); } } // Check for URL changes function setupUrlChangeDetection() { try { let lastUrl = window.location.href; let checkUrlInterval = null; // Create a function that checks if the URL has changed const checkForUrlChange = () => { if (lastUrl !== window.location.href) { log('URL changed from:', lastUrl, 'to:', window.location.href); lastUrl = window.location.href; // Add a delay to let the page render setTimeout(() => { ensureButtonExists(); }, 500); } }; // Check every 200ms - less frequent to reduce CPU usage checkUrlInterval = setInterval(checkForUrlChange, 200); // Clear interval when page unloads to prevent memory leaks window.addEventListener('unload', () => { if (checkUrlInterval) { clearInterval(checkUrlInterval); } }); } catch (err) { console.error("[Agnai Import Button] Error setting up URL change detection:", err); } } // Add CSS to the document for any global styles function addGlobalStyles() { try { const styleElement = document.createElement('style'); styleElement.textContent = ` .agnai-import-button { transition: all 0.3s ease !important; } `; document.head.appendChild(styleElement); } catch (err) { console.error("[Agnai Import Button] Error adding global styles:", err); } } // Initialize with a self-cleaning periodic check let periodicCheckInterval = null; function startPeriodicCheck() { if (periodicCheckInterval) { clearInterval(periodicCheckInterval); } periodicCheckInterval = setInterval(() => { if (isCharacterPage()) { const result = ensureButtonExists(); // If on a numeric ID page and the button exists but we're still waiting for the image if (getSiteType() === 'chub' && isNumericIdPage() && !extractIdentifierFromImage()) { log('Still waiting for image to load on numeric ID page'); } // If the button was successfully added and we're not on a numeric ID page waiting for image else if (result && !(getSiteType() === 'chub' && isNumericIdPage() && !extractIdentifierFromImage())) { log('Button successfully added, stopping periodic check'); clearInterval(periodicCheckInterval); periodicCheckInterval = null; } } else { clearInterval(periodicCheckInterval); periodicCheckInterval = null; } }, CHECK_INTERVAL); // Clean up on page unload window.addEventListener('unload', () => { if (periodicCheckInterval) { clearInterval(periodicCheckInterval); } }); } // Initialize function init() { try { log('Script initialized'); log(`Current site: ${getSiteType()}`); // Add global styles addGlobalStyles(); // Setup URL change detection setupUrlChangeDetection(); // Setup DOM observer setupObserver(); // Initial check ensureButtonExists(); // Start periodic check startPeriodicCheck(); } catch (err) { console.error("[Agnai Import Button] Error initializing script:", err); } } // Wait for the page to fully load before initializing if (document.readyState === 'loading') { window.addEventListener('load', init); } else { init(); } })();