您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Automatically decide whether a Google search query should open in AI mode by adding udm=50, or stay as normal search, based on URL query analysis (script detection + rule-based scoring). Toggle UI retained.
// ==UserScript== // @name Google AI Mode Auto Switcher // @namespace http://tampermonkey.net/ // @version 0.9 // @description Automatically decide whether a Google search query should open in AI mode by adding udm=50, or stay as normal search, based on URL query analysis (script detection + rule-based scoring). Toggle UI retained. // @author djshigel // @match https://www.google.com/* // @match https://www.google.co.jp/* // @match https://www.google.*/* // @grant GM_getValue // @grant GM_setValue // @license MIT // ==/UserScript== (function() { 'use strict'; // Configuration const sessionKey = 'gm_ai_auto_redirect_flag'; const STORAGE_KEY_ENABLED = 'gm_ai_auto_enabled_v0_5'; // Auto tooltip timing (per request) const AUTO_TOOLTIP_DELAY_MS = 1000; // wait 1s before auto tooltip flow const AUTO_TOOLTIP_SHOW_MS = 2000; // show message for 2s const AUTO_TOOLTIP_RESTORE_DELAY_MS = 0; // Immediately after restoring tip, return to non-hover // ----------------------------- // Simple hash function for session keys (handles multi-byte characters) // ----------------------------- function simpleHash(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return Math.abs(hash).toString(36); } // ----------------------------- // Storage helpers (use GM_getValue/GM_setValue when available) // ----------------------------- function setStorageEnabled(v) { try { if (typeof GM_setValue === 'function') { GM_setValue(STORAGE_KEY_ENABLED, v ? '1' : '0'); return; } } catch (e) { /* ignore */ } try { localStorage.setItem(STORAGE_KEY_ENABLED, v ? '1' : '0'); } catch (e) {} } function getStorageEnabled() { try { if (typeof GM_getValue === 'function') { const val = GM_getValue(STORAGE_KEY_ENABLED, '0'); return val === '1'; } } catch (e) { /* ignore */ } try { return localStorage.getItem(STORAGE_KEY_ENABLED) === '1'; } catch (e) { return false; } } // ----------------------------- // Mobile detection // ----------------------------- function isMobile() { return navigator.userAgent.includes('Mobile'); } // ----------------------------- // Navigation type detection // ----------------------------- function getNavigationType() { try { // Modern way using Navigation Timing API const perfEntries = performance.getEntriesByType('navigation'); if (perfEntries && perfEntries.length > 0) { const navEntry = perfEntries[0]; // type: 'navigate', 'reload', 'back_forward', 'prerender' return navEntry.type; } } catch (e) { /* ignore */ } // Fallback: check performance.navigation (deprecated but still works) try { if (performance.navigation) { // 0: TYPE_NAVIGATE, 1: TYPE_RELOAD, 2: TYPE_BACK_FORWARD switch (performance.navigation.type) { case 0: return 'navigate'; case 1: return 'reload'; case 2: return 'back_forward'; default: return 'navigate'; } } } catch (e) { /* ignore */ } return 'navigate'; // default fallback } // ----------------------------- // Decision logic (Score-based) // Scoring elements: // - Average token length × weight // - Token count × weight // - Question marks (+30 points) // - Japanese particles (+10 points) // - Sentence ending patterns (+10 points) // - Too short penalty (-20 points) // Threshold: Score >= 30 = AI mode // // Examples: // "ごちうさ op 2期 歌詞" → score < 30 → nav (short keywords) // "北海道でのクワガタの捕まえ方教えて" → score 60+ → ai (sentence) // "Python エラー解決" → score 15 → nav (short keywords) // "Pythonでファイルが読み込めないエラーを解決する方法" → score 45+ → ai (long sentence) // ----------------------------- function decideModeByQuery(q) { if (!q) return 'nav'; const normalized = q.trim().replace(/\s+/g, ' '); const tokens = normalized.split(' ').filter(Boolean); const tokenCount = tokens.length; const totalLength = normalized.length; // Quick URL-like detection: dot or slash likely a URL -> nav if (/[./]/.test(normalized)) return 'nav'; // Calculate complexity score let score = 0; // 1. Average token length (weight: important) const avgTokenLength = totalLength / Math.max(tokenCount, 1); // For Japanese/Chinese (no spaces), consider character-based scoring const hasJapaneseChinese = /[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/.test(normalized); if (hasJapaneseChinese) { // Japanese/Chinese scoring: // - Single token >= 9 chars = +50 points (likely sentence) // - Average token length × 8 // - Token count × 3 if (tokenCount === 1 && totalLength >= 9) { score += 50; } // Multiple tokens: check if keywords or sentence if (tokenCount >= 2) { score += avgTokenLength * 8; score += tokenCount * 3; } } else { // English/Latin scoring: // - Average token length × 3 // - Token count × 5 // - Length > 40 chars = +20 points score += avgTokenLength * 3; score += tokenCount * 5; if (totalLength > 40) score += 20; } // 2. Question marks = strong AI signal (+30 points) if (/[??]/.test(normalized)) { score += 30; } // 3. Sentence-like patterns // Japanese particles that indicate sentences (+10 points) if (/[をはがでにへとのも]/.test(normalized)) { score += 10; } // English question words/auxiliaries (+15 points) if (/\b(what|how|why|when|where|who|can|could|should|would|will|is|are|was|were)\b/i.test(normalized)) { score += 15; } // 4. Too short penalty (likely navigation, -20 points) if (totalLength < 6) { score -= 20; } // 5. Sentence ending patterns (+10 points) if (/[。..!!]$/.test(normalized)) { score += 10; } // Japanese sentence endings (+10 points) if (/て$|か$|す$|ます$|です$|した$|ました$/.test(normalized)) { score += 10; } // Decision threshold // Score >= 60 = AI mode // Score < 60 = nav mode return score >= 60 ? 'ai' : 'nav'; } // ----------------------------- // Toggle UI // ----------------------------- function createToggleUI() { // Skip UI creation on mobile devices when in AI mode const currentUrl = new URL(window.location.href); const params = currentUrl.searchParams; const hasUdm50 = params.get('udm') === '50'; if (isMobile() && hasUdm50) { return null; // Hide toggle on mobile when in AI mode } const existing = document.getElementById('ai-mode-toggle-container'); if (existing) return existing; // reuse const darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; let isEnabled = getStorageEnabled(); const container = document.createElement('div'); container.id = 'ai-mode-toggle-container'; container.style.position = 'fixed'; container.style.bottom = '20px'; container.style.left = '20px'; container.style.zIndex = '2147483647'; container.style.opacity = '0.1'; container.style.transform = 'scale(0.7)'; container.style.transformOrigin = 'bottom left'; container.style.transition = 'opacity 0.3s, transform 0.3s, box-shadow 0.3s'; container.style.display = 'flex'; container.style.alignItems = 'center'; container.style.gap = '12px'; container.style.padding = '8px 16px'; container.style.borderRadius = '24px'; container.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)'; container.style.background = darkMode ? 'rgba(48,49,52,0.95)' : 'rgba(255,255,255,0.95)'; container.style.border = '1px solid ' + (darkMode ? '#5f6368' : '#dadce0'); container.style.fontFamily = 'Arial, sans-serif'; container.style.fontSize = '14px'; container.innerHTML = ` <span id="ai-mode-label" style="color: ${darkMode ? '#e8eaed' : '#5f6368'}; white-space:nowrap">Auto AI Mode</span> <div id="ai-mode-toggle" style="width:48px;height:24px;border-radius:24px;position:relative;cursor:pointer;background:${isEnabled ? '#1a73e8' : (darkMode ? '#5f6368' : '#ccc')}"> <div id="ai-mode-toggle-knob" style="width:18px;height:18px;background:#fff;border-radius:50%;position:absolute;top:3px;left:${isEnabled ? '27px' : '3px'};transition:left 0.3s;box-shadow:0 2px 4px rgba(0,0,0,0.2)"></div> </div> <span id="ai-mode-status" style="color:${isEnabled ? '#1a73e8' : (darkMode ? '#9aa0a6' : '#80868b')};font-weight:500;white-space:nowrap">${isEnabled ? 'ON' : 'OFF'}</span> `; let isHover = false; let autoTooltipTimers = []; function clearAutoTooltipTimers() { while (autoTooltipTimers.length) { clearTimeout(autoTooltipTimers.shift()); } container.dataset.autoTooltipActive = '0'; } container.addEventListener('mouseover', () => { isHover = true; clearAutoTooltipTimers(); container.style.opacity = '1'; container.style.transform = 'scale(1)'; }); container.addEventListener('mouseout', () => { isHover = false; container.style.opacity = '0.1'; container.style.transform = 'scale(0.7)'; }); container.querySelector('#ai-mode-toggle').addEventListener('click', () => { isEnabled = !isEnabled; setStorageEnabled(isEnabled); const knob = container.querySelector('#ai-mode-toggle-knob'); const status = container.querySelector('#ai-mode-status'); container.querySelector('#ai-mode-toggle').style.background = isEnabled ? '#1a73e8' : (darkMode ? '#5f6368' : '#ccc'); knob.style.left = isEnabled ? '27px' : '3px'; status.textContent = isEnabled ? 'ON' : 'OFF'; status.style.color = isEnabled ? '#1a73e8' : (darkMode ? '#9aa0a6' : '#80868b'); const currentUrl = new URL(window.location.href); const params = currentUrl.searchParams; const hasSearchQuery = (params.has('q') && params.get('q').trim().length > 0) || (params.has('as_q') && params.get('as_q').trim().length > 0); const hasUdm50 = params.get('udm') === '50'; const isNormalSearch = !hasUdm50; if (isEnabled && !hasUdm50 && hasSearchQuery && isNormalSearch) { try { // Clear session storage for new redirect const q = params.get('q') || params.get('as_q') || ''; const isSiri = params.has('as_q') && !params.has('q'); const queryHash = simpleHash(q); const sessionKeyToUse = isSiri ? sessionKey + '_siri_' + queryHash : sessionKey + '_' + queryHash; sessionStorage.removeItem(sessionKeyToUse); } catch (e) {} // For Siri searches, move as_q to q parameter for AI mode compatibility if (params.has('as_q') && !params.has('q')) { const query = params.get('as_q'); params.set('q', query); params.delete('as_q'); params.delete('as_occt'); params.delete('as_qdr'); } params.set('udm', '50'); currentUrl.search = params.toString(); window.location.href = currentUrl.toString(); } }); // Expose auto-tooltip flow but guard against double runs container._triggerAutoTooltip = function() { if (isHover) return; // user hovered, cancel if (container.dataset.autoTooltipActive === '1') return; // already active container.dataset.autoTooltipActive = '1'; clearAutoTooltipTimers(); const originalOpacity = container.style.opacity; const originalTransform = container.style.transform; const labelEl = container.querySelector('#ai-mode-label'); const originalLabel = labelEl ? labelEl.textContent : ''; const t1 = setTimeout(() => { if (isHover) { clearAutoTooltipTimers(); if (labelEl) labelEl.textContent = originalLabel; return; } container.style.opacity = '1'; container.style.transform = 'scale(1)'; if (labelEl) labelEl.textContent = 'Long queries activate AI mode'; const t2 = setTimeout(() => { if (isHover) { clearAutoTooltipTimers(); if (labelEl) labelEl.textContent = originalLabel; return; } if (labelEl) labelEl.textContent = originalLabel; const t3 = setTimeout(() => { if (isHover) { clearAutoTooltipTimers(); return; } container.style.opacity = originalOpacity; container.style.transform = originalTransform; container.dataset.autoTooltipActive = '0'; }, AUTO_TOOLTIP_RESTORE_DELAY_MS); autoTooltipTimers.push(t3); }, AUTO_TOOLTIP_SHOW_MS); autoTooltipTimers.push(t2); }, AUTO_TOOLTIP_DELAY_MS); autoTooltipTimers.push(t1); }; try { document.body.appendChild(container); } catch (e) { console.error('Failed to append AI toggle UI', e); } container.dataset.autoTooltipActive = '0'; return container; } // Main flow (function main() { try { const currentUrl = new URL(window.location.href); const params = currentUrl.searchParams; // Get query from either 'q' or 'as_q' parameter (Siri uses as_q) const q = params.get('q') || params.get('as_q') || ''; const isSiriSearch = params.has('as_q') && !params.has('q'); if (!q) return; // nothing to do on pages without a search query const userEnabled = getStorageEnabled(); const hasUdm50 = params.get('udm') === '50'; const desiredMode = decideModeByQuery(q); // 'ai'|'nav' // Special search detection const specialUdmValues = ['2','7','28','36','39']; // images, videos, shopping, etc. const specialTbmValues = ['nws','flm','fin','lcl','isch','vid','shop','bks']; const udmValue = params.get('udm'); const tbmValue = params.get('tbm'); const hasSpecialUdm = udmValue && specialUdmValues.includes(udmValue); const hasSpecialTbm = tbmValue && specialTbmValues.includes(tbmValue); const isNormalSearch = !hasSpecialUdm && !hasSpecialTbm; // Check navigation type - only redirect on navigate/reload, not back_forward const navType = getNavigationType(); const isBackForward = navType === 'back_forward'; // Check if this is from tab navigation (source=lnms indicates tab switching) const isFromTabNavigation = params.get('source') === 'lnms'; // Check if this is from a new tab action (sa=X parameter is often present) const hasNewTabIndicator = params.has('sa') && params.get('sa') === 'X'; // Check if this is a new tab opened from Google search // Exception: Siri searches (as_q) should be treated as new searches, not tab navigation const isFromGoogleNewTab = !isSiriSearch && ( (document.referrer && document.referrer.includes('google.com') && document.referrer.includes('/search')) || isFromTabNavigation || hasNewTabIndicator); // Don't redirect if: // 1. User navigated back/forward (tab switching) // 2. Already in AI mode // 3. Special search type // 4. Feature disabled // 5. Query doesn't warrant AI mode // 6. New tab opened from Google search results or tab navigation (except Siri) const shouldRedirect = (desiredMode === 'ai') && userEnabled && isNormalSearch && !hasUdm50 && !isBackForward && !isFromGoogleNewTab; if (shouldRedirect) { try { // Use a more specific session key that includes the query to prevent duplicate redirects // For Siri searches, use a different approach since they don't follow normal tab flow // Use simple hash to handle multi-byte characters const queryHash = simpleHash(q); const sessionKeyToUse = isSiriSearch ? sessionKey + '_siri_' + queryHash : sessionKey + '_' + queryHash; const alreadyRedirected = sessionStorage.getItem(sessionKeyToUse) === 'true'; if (!alreadyRedirected) { sessionStorage.setItem(sessionKeyToUse, 'true'); // For Siri searches, move as_q to q parameter for AI mode compatibility if (isSiriSearch) { params.set('q', q); params.delete('as_q'); params.delete('as_occt'); params.delete('as_qdr'); } params.set('udm', '50'); currentUrl.search = params.toString(); window.location.href = currentUrl.toString(); return; } } catch (e) { console.error('Redirect error:', e); } } // Create or reuse toggle UI (returns null on mobile+AI mode) const container = createToggleUI(); // If we started in nav mode and have UI, trigger the auto-tooltip flow to inform users if (container && desiredMode === 'nav') { try { container._triggerAutoTooltip(); } catch (e) {} } } catch (e) { console.error('AI Mode Auto Switcher main error', e); } })(); })();