您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Redirects searches with custom bangs and DuckDuckGo bangs queried on-demand
// ==UserScript== // @name Enhanced Search !Bang Redirects v2 // @namespace http://your.namespace.here // @version 3.5 // @description Redirects searches with custom bangs and DuckDuckGo bangs queried on-demand // @match *://*.google.com/* // @match *://*.bing.com/* // @match *://startpage.com/* // @match *://*.brave.com/* // @match *://*.ecosia.org/* // @match *://*.duckduckgo.com/* // @match *://*.qwant.com/* // @match *://*.mullvad.net/* // @match *://*.mojeek.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @grant GM.xmlHttpRequest // @grant GM_xmlhttpRequest // @connect duckduckgo.com // @run-at document-start // @license MIT // ==/UserScript== /* The MIT License (MIT) Copyright (c) 2025 ShadowTux Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ (function() { 'use strict'; // Cross-browser compatibility layer const GM = (typeof window.GM !== 'undefined') ? window.GM : {}; // Unified API functions for Firefox/Chrome compatibility function GM_setValue(key, value) { if (typeof window.GM_setValue !== 'undefined') { return window.GM_setValue(key, value); } else if (typeof window.GM.setValue !== 'undefined') { return window.GM.setValue(key, value); } else { console.warn('GM_setValue not available, using localStorage fallback'); try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch (e) { console.error('Failed to set value:', e); return false; } } } function GM_getValue(key, defaultValue) { if (typeof window.GM_getValue !== 'undefined') { return window.GM_getValue(key, defaultValue); } else if (typeof window.GM.getValue !== 'undefined') { return window.GM.getValue(key, defaultValue); } else { console.warn('GM_getValue not available, using localStorage fallback'); try { const value = localStorage.getItem(key); return value ? JSON.parse(value) : defaultValue; } catch (e) { console.error('Failed to get value:', e); return defaultValue; } } } function GM_xmlhttpRequest(options) { if (typeof window.GM_xmlhttpRequest !== 'undefined') { return window.GM_xmlhttpRequest(options); } else if (typeof window.GM.xmlHttpRequest !== 'undefined') { return window.GM.xmlHttpRequest(options); } else { console.warn('GM_xmlhttpRequest not available, using fetch fallback'); // Fallback to fetch API const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), options.timeout || 15000); fetch(options.url, { method: options.method || 'GET', headers: options.headers || {}, signal: controller.signal }) .then(response => { clearTimeout(timeoutId); // Handle response.text() as a Promise return response.text().then(text => { if (options.onload) { options.onload({ status: response.status, responseText: text, statusText: response.statusText }); } }); }) .catch(error => { clearTimeout(timeoutId); if (options.onerror) { options.onerror(error); } }); } } // Lightweight JSON fetch via GM (avoids CORS issues) function fetchJsonViaGM(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url, headers: { 'Accept': 'application/json, text/javascript, */*; q=0.01' }, onload: function(response) { try { const text = response.responseText; const data = JSON.parse(text); resolve(data); } catch (e) { reject(e); } }, onerror: function(err) { reject(err); }, ontimeout: function() { reject(new Error('GM request timeout')); }, timeout: 15000 }); }); } // Configuration const DDG_BANG_CACHE_KEY = 'ddg_bang_cache_v3'; const DDG_BANG_CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds // Custom bangs that take priority over DuckDuckGo bangs const CUSTOM_BANGS = { // AI service bangs '!chatgpt': { url: 'https://chatgpt.com/?q={query}', description: 'ChatGPT AI Assistant', category: 'AI' }, '!chat': { url: 'https://chatgpt.com/?q={query}', description: 'ChatGPT AI Assistant', category: 'AI' }, '!claude': { url: 'https://claude.ai', description: 'Claude AI Assistant', category: 'AI' }, '!t3': { url: 'https://t3.chat', description: 'T3 Chat AI Assistant', category: 'AI' }, '!t3chat': { url: 'https://t3.chat', description: 'T3 Chat AI Assistant', category: 'AI' }, '!summary': { url: 'https://search.brave.com/search?q={query}&source=llmSuggest&summary=1', description: 'Brave Search with AI Summary', category: 'AI' }, '!perp': { url: 'https://www.perplexity.ai/search?q={query}', description: 'Perplexity AI Search', category: 'AI' }, '!youai': { url: 'https://you.com/search?q={query}&fromSearchBar=true&tbm=youchat&chatMode=default', description: 'You.com AI Chat', category: 'AI' }, '!phind': { url: 'https://www.phind.com/search?q={query}&searchMode=auto&allowMultiSearch=true', description: 'Phind AI for Developers', category: 'AI' }, '!felo': { url: 'https://felo.ai/search?q={query}', description: 'Felo AI Search', category: 'AI' }, '!ecoai': { url: 'https://www.ecosia.org/chat?q={query}', description: 'Ecosia AI Chat', category: 'AI' }, '!mistral': { url: 'https://chat.mistral.ai/chat?q={query}&mode=ai', description: 'Mistral AI Chat', category: 'AI' }, '!mis': { url: 'https://chat.mistral.ai/chat?q={query}&mode=ai', description: 'Mistral AI Chat', category: 'AI' }, // Search engine bangs '!g': { url: 'https://www.google.com/search?q={query}', description: 'Google Search', category: 'Search' }, '!s': { url: 'https://www.startpage.com/sp/search?query={query}', description: 'Startpage Search', category: 'Search' }, '!sp': { url: 'https://www.startpage.com/sp/search?query={query}', description: 'Startpage Search', category: 'Search' }, '!yt': { url: 'https://www.youtube.com/results?search_query={query}', description: 'YouTube Search', category: 'Multimedia' }, '!w': { url: 'https://en.wikipedia.org/wiki/Special:Search?search={query}', description: 'Wikipedia Search', category: 'Research' }, '!nixpkgs': { url: 'https://search.nixos.org/packages?query={query}', description: 'NixOS Packages', category: 'Tech' }, '!ddg': { url: 'https://duckduckgo.com/?q={query}', description: 'DuckDuckGo Search', category: 'Search' }, '!qw': { url: 'https://www.qwant.com/?q={query}&t=web', description: 'Qwant Search', category: 'Search' }, '!qwant': { url: 'https://www.qwant.com/?q={query}&t=web', description: 'Qwant Search', category: 'Search' }, '!leta': { url: 'https://leta.mullvad.net/search?q={query}&engine=brave', description: 'Mullvad Leta Search', category: 'Search' }, // Mojeek bangs '!mj': { url: 'https://www.mojeek.com/search?q={query}&theme=dark', description: 'Mojeek Search', category: 'Search' }, '!mojeek': { url: 'https://www.mojeek.com/search?q={query}&theme=dark', description: 'Mojeek Search', category: 'Search' }, '!mjs': { url: 'https://www.mojeek.com/search?q={query}&theme=dark&fmt=summary', description: 'Mojeek Search with Summary', category: 'Search' }, '!sum': { url: 'https://www.mojeek.com/search?q={query}&theme=dark&fmt=summary', description: 'Mojeek Search with Summary', category: 'Search' } }; // Global bangs storage let allBangs = { ...CUSTOM_BANGS }; // Function to extract URL parameters function getUrlParameter(name) { name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); var regex = new RegExp('[\?&]' + name + '=([^&#]*)'); var results = regex.exec(location.search); return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); } // Helper function to perform a direct redirect function performRedirect(url, query) { const finalUrl = url.replace('{query}', encodeURIComponent(query)); // For URLs that don't contain {query} placeholder, don't add extra parameters let redirectUrl; if (url.includes('{query}')) { const separator = finalUrl.includes('?') ? '&' : '?'; redirectUrl = finalUrl + separator + 'bang_redirect=1'; } else { // For simple redirects like Claude.ai, just use the URL as-is redirectUrl = finalUrl; } console.log("Redirecting to:", redirectUrl); window.location.replace(redirectUrl); } // Function to query DuckDuckGo for a specific bang async function queryDuckDuckGoBang(bangTrigger) { try { // Remove the ! from the bang trigger const bangName = bangTrigger.substring(1); // Check cache first const cacheKey = `${DDG_BANG_CACHE_KEY}_${bangName}`; const cachedBang = GM_getValue(cacheKey, null); const cacheTimestamp = GM_getValue(`${cacheKey}_timestamp`, 0); const now = Date.now(); if (cachedBang && (now - cacheTimestamp) < DDG_BANG_CACHE_DURATION) { console.log(`📦 Using cached DuckDuckGo bang: ${bangTrigger}`); return cachedBang; } console.log(`🔍 Querying DuckDuckGo bang.js for bang: ${bangTrigger}`); // Query DuckDuckGo's bang.js API (via GM to avoid CORS) const data = await fetchJsonViaGM('https://duckduckgo.com/bang.js'); if (data && data.length > 0) { // Find the exact match by trigger const exactMatch = data.find(item => item.t === bangName); if (exactMatch) { console.log(`✅ Found DuckDuckGo bang: ${bangTrigger} -> ${exactMatch.s} (${exactMatch.c})`); const bangData = { url: exactMatch.u, description: exactMatch.s, domain: exactMatch.d, category: exactMatch.c || 'DuckDuckGo', subcategory: exactMatch.sc || '' }; // Cache the result GM_setValue(cacheKey, bangData); GM_setValue(`${cacheKey}_timestamp`, now); return bangData; } } console.log(`❌ No DuckDuckGo bang found for: ${bangTrigger}`); return null; } catch (error) { console.error(`Error querying DuckDuckGo for bang ${bangTrigger}:`, error); return null; } } // Function to test DuckDuckGo bang query async function testDuckDuckGoBang(bangTrigger) { console.log(`Testing DuckDuckGo bang query for: ${bangTrigger}`); const result = await queryDuckDuckGoBang(bangTrigger); if (result) { console.log(`✅ Bang found:`, result); } else { console.log(`❌ Bang not found`); } return result; } // Function to search through all available DuckDuckGo bangs async function searchDuckDuckGoBangs(searchTerm) { try { console.log(`🔍 Searching DuckDuckGo bangs for: "${searchTerm}"`); const data = await fetchJsonViaGM('https://duckduckgo.com/bang.js'); if (data && data.length > 0) { // Search through all bangs for matches const matches = data.filter(bang => bang.t.toLowerCase().includes(searchTerm.toLowerCase()) || bang.s.toLowerCase().includes(searchTerm.toLowerCase()) || bang.d.toLowerCase().includes(searchTerm.toLowerCase()) || (bang.c && bang.c.toLowerCase().includes(searchTerm.toLowerCase())) ); console.log(`Found ${matches.length} matching bangs for "${searchTerm}"`); // Show first 10 matches const displayMatches = matches.slice(0, 10); displayMatches.forEach((bang, index) => { console.log(`${index + 1}. !${bang.t} -> ${bang.s} (${bang.d}) - ${bang.c}`); }); if (matches.length > 10) { console.log(`... and ${matches.length - 10} more matches`); } return matches; } return []; } catch (error) { console.error(`Error searching DuckDuckGo bangs:`, error); return []; } } // Function to process bangs async function processBang(query) { if (!query) return false; console.log(`Processing query for bangs: "${query}"`); // Check for redirection loops - removed timestamp check // Bang redirects are now handled by the bang_redirect parameter // Check if query contains bang_redirect parameter (prevents processing already redirected URLs) if (query.includes('bang_redirect=1')) { console.log("Skipping bang processing - already redirected"); return false; } // First check custom bangs (they take priority) const customBangsList = Object.keys(CUSTOM_BANGS).sort((a, b) => b.length - a.length); for (const bang of customBangsList) { const bangIndex = query.indexOf(bang); if (bangIndex !== -1) { const afterBang = query.substring(bangIndex + bang.length); // Only process if there's a space after the bang or if it's at the end if (afterBang === '' || afterBang.startsWith(' ')) { const cleanQuery = query.replace(bang, '').trim(); console.log(`Found custom bang: ${bang} -> ${CUSTOM_BANGS[bang].description}, query: "${cleanQuery}"`); console.log(`Bang URL: ${CUSTOM_BANGS[bang].url}`); // Perform redirect performRedirect(CUSTOM_BANGS[bang].url, cleanQuery); return true; } } } // If no custom bang found, check for DuckDuckGo bangs // Extract potential bang from query (look for !bang pattern) const bangMatch = query.match(/!(\w+)/); if (bangMatch) { const bangTrigger = '!' + bangMatch[1]; const afterBang = query.substring(bangMatch.index + bangTrigger.length); // Only process if there's a space after the bang or if it's at the end if (afterBang === '' || afterBang.startsWith(' ')) { const cleanQuery = query.replace(bangTrigger, '').trim(); console.log(`Querying DuckDuckGo for bang: ${bangTrigger}`); // Query DuckDuckGo for this specific bang const ddgBang = await queryDuckDuckGoBang(bangTrigger); if (ddgBang) { console.log(`Found DuckDuckGo bang: ${bangTrigger} -> ${ddgBang.description}, query: "${cleanQuery}"`); // Convert DuckDuckGo URL format to our format let redirectUrl = ddgBang.url; if (redirectUrl.includes('{{{s}}}')) { redirectUrl = redirectUrl.replace(/{{{s}}}/g, '{query}'); } // Perform redirect performRedirect(redirectUrl, cleanQuery); return true; } else { console.log(`No DuckDuckGo bang found for: ${bangTrigger}`); } } } console.log("No matching bang found"); return false; } // Main execution function async function main() { // Extract the search query from various search engines const query = getUrlParameter('q') || getUrlParameter('query') || getUrlParameter('search_query'); if (!query) return; console.log(`Processing query: "${query}"`); // Special case for Ecosia shopping search const isEcosia = window.location.hostname.includes('ecosia.org'); const isShoppingSearch = window.location.pathname.includes('/shopping'); if (isEcosia) { console.log(`🌱 Ecosia detected - hostname: ${window.location.hostname}, path: ${window.location.pathname}`); console.log(`🌱 Query: "${query}"`); if (isShoppingSearch && query) { console.log("🌱 Redirecting Ecosia shopping search to Startpage"); window.location.replace('https://www.startpage.com/sp/search?query=' + encodeURIComponent(query) + '&bang_redirect=1'); return; } // For Ecosia, process bangs immediately to prevent search execution console.log("🌱 Processing bang on Ecosia immediately"); if (await processBang(query)) { return; // Bang was processed, don't continue } } // Process the bang await processBang(query); } // Debug function to show loaded bangs function showLoadedBangs() { const customCount = Object.keys(CUSTOM_BANGS).length; console.log(`🔥 Enhanced Search Bang Redirects v3.1 Status:`); console.log(` Custom bangs: ${customCount}`); console.log(` DuckDuckGo bangs: Queried on-demand with caching`); console.log(` Cache duration: ${DDG_BANG_CACHE_DURATION / (24 * 60 * 60 * 1000)} days`); // Show some example custom bangs const exampleBangs = Object.keys(CUSTOM_BANGS).slice(0, 10); console.log(` Example custom bangs: ${exampleBangs.join(', ')}`); console.log(` Try: testDuckDuckGoBang('!a2') to test DuckDuckGo bang query`); console.log(` Try: searchDuckDuckGoBangs('alternative') to search for bangs`); } // Make debug function available globally window.showBangs = showLoadedBangs; // Test function to check DuckDuckGo bang query window.testDuckDuckGoBang = testDuckDuckGoBang; // Function to search through all DuckDuckGo bangs window.searchDuckDuckGoBangs = searchDuckDuckGoBangs; // Function to clear DuckDuckGo bang cache window.clearDuckDuckGoCache = function() { console.log("🗑️ Clearing DuckDuckGo bang cache..."); // Get all GM keys and clear DDG bang cache keys const allKeys = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(DDG_BANG_CACHE_KEY)) { allKeys.push(key); } } // Clear each cache key allKeys.forEach(key => { GM_setValue(key, null); console.log(`Cleared cache key: ${key}`); }); console.log("🔄 DuckDuckGo bang cache cleared!"); console.log("Next bang queries will fetch fresh data from DuckDuckGo"); }; // Test function to check specific bangs window.testBang = function(bangName) { console.log(`Testing bang: ${bangName}`); // Check custom bangs first if (CUSTOM_BANGS[bangName]) { console.log(`✅ Custom bang found: ${bangName}`); console.log(` URL: ${CUSTOM_BANGS[bangName].url}`); console.log(` Description: ${CUSTOM_BANGS[bangName].description}`); console.log(` Category: ${CUSTOM_BANGS[bangName].category}`); return; } // Check if it's a DuckDuckGo bang if (bangName.startsWith('!')) { console.log(`🔍 Testing DuckDuckGo bang: ${bangName}`); console.log(`Use testDuckDuckGoBang('${bangName}') to query DuckDuckGo directly`); } else { console.log(`❌ Bang not found: ${bangName}`); console.log(`Available custom bangs starting with ${bangName.substring(0, 3)}:`); const similarBangs = Object.keys(CUSTOM_BANGS).filter(b => b.startsWith(bangName.substring(0, 3))); console.log(similarBangs.slice(0, 10)); } }; // Initialize and run (async function init() { try { console.log("🚀 Initializing Enhanced Search Bang Redirects v3.1..."); // Browser detection const userAgent = navigator.userAgent; const isFirefox = userAgent.includes('Firefox'); const isChrome = userAgent.includes('Chrome') && !userAgent.includes('Edg'); console.log(`🌐 Browser detected: ${isFirefox ? 'Firefox' : isChrome ? 'Chrome' : 'Other'}`); // Check GM API availability console.log(`🔧 GM API Status:`, { GM_setValue: typeof GM_setValue !== 'undefined', GM_getValue: typeof GM_getValue !== 'undefined', GM_xmlhttpRequest: typeof GM_xmlhttpRequest !== 'undefined' }); // Initialize with custom bangs only allBangs = CUSTOM_BANGS; // Process current page immediately await main(); showLoadedBangs(); console.log("✅ Bang system ready! Type 'showBangs()' in console for status."); console.log("🔍 DuckDuckGo bangs are queried from bang.js API and cached for 7 days"); } catch (error) { console.error("❌ Error initializing bang redirects:", error); // Ensure we always have at least custom bangs allBangs = CUSTOM_BANGS; await main(); } })(); })();