// ==UserScript== // @name 🌐 搜索中心增强 // @name:en 🌐 Search Hub Enhancer // @namespace https://greasyfork.org/zh-CN/users/1454800 // @version 1.0.20 // @description 快速切换搜索引擎的工具栏,可拖动到顶部/底部/左侧/右侧,支持水平/垂直布局,内容少时居中,内容多时可滑动,设置面板宽度随屏幕动态调整 // @description:en A draggable toolbar for quick switching between search engines, supports top/bottom/left/right positions with horizontal/vertical layouts, centered when content is minimal, scrollable when content is extensive, settings panel width adjusts dynamically with screen size // @author Aiccest // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @noframes // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/534928/%F0%9F%8C%90%20%E6%90%9C%E7%B4%A2%E4%B8%AD%E5%BF%83%E5%A2%9E%E5%BC%BA.user.js // @updateURL https://update.greasyfork.icu/scripts/534928/%F0%9F%8C%90%20%E6%90%9C%E7%B4%A2%E4%B8%AD%E5%BF%83%E5%A2%9E%E5%BC%BA.meta.js // ==/UserScript== (function () { 'use strict'; // 调试开关 const DEBUG = true; // 配置 const CONFIG = { STORAGE_KEY: 'search_hub_engines', POSITION_KEY: 'toolbar_position', DEBOUNCE_MS: 400, ANIMATION_MS: 300, WEIBO_CONTAINER_ID: '100103', }; // 语言包 const i18n = { 'zh-CN': { scriptName: '🌐 搜索中心增强', scriptDescription: '快速切换搜索引擎的工具栏,可自定义引擎', settingsTitle: '🌐 搜索引擎设置', addButton: '添加', saveButton: '保存', closeButton: '关闭', namePlaceholder: '名称', urlPlaceholder: '包含 %s 的URL', alertRequired: '名称和URL为必填项!', alertUrlFormat: 'URL必须包含%s占位符!', alertInvalidUrl: '无效的URL!', alertMinEngines: '至少需要一个搜索引擎!', alertNotSearchPage: '当前页面不是搜索页面,无法添加为搜索引擎!', alertNoEngineConfig: '无法检测当前页面的搜索引擎配置!', menuAddEngine: '🌐 添加当前页面为搜索引擎' }, 'en-US': { scriptName: '🌐 Search Hub Enhancer', scriptDescription: 'A toolbar for quickly switching search engines, with customizable engines', settingsTitle: '🌐 Search Engine Settings', addButton: 'Add', saveButton: 'Save', closeButton: 'Close', namePlaceholder: 'Name', urlPlaceholder: 'URL containing %s', alertRequired: 'Name and URL are required!', alertUrlFormat: 'URL must contain %s placeholder!', alertInvalidUrl: 'Invalid URL!', alertMinEngines: 'At least one search engine is required!', alertNotSearchPage: 'This page is not a search page and cannot be added as a search engine!', alertNoEngineConfig: 'Cannot detect the search engine configuration for this page!', menuAddEngine: '🌐 Add Current Page as Search Engine' } }; // 获取系统语言 const getLanguage = () => { const lang = navigator.language || navigator.userLanguage; return lang.startsWith('zh') ? 'zh-CN' : 'en-US'; }; const lang = getLanguage(); // 工具栏 CSS const TOOLBAR_CSS = ` :host { all: initial; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important; font-weight: normal !important; font-size: 14px !important; --bg-color: rgba(255, 255, 255, 0.95); --text-color: #1f2937; --border-color: #e5e7eb; --hover-bg: #f9fafb; --btn-bg: #f9fafb; --btn-active-bg: #e5e7eb; --dragging-bg: rgba(200, 200, 200, 0.3); } @media (prefers-color-scheme: dark) { :host { --bg-color: rgba(31, 41, 55, 0.95); --text-color: #e5e7eb; --border-color: #4b5563; --hover-bg: #374151; --btn-bg: #374151; --btn-active-bg: #4b5563; --dragging-bg: rgba(100, 100, 100, 0.3); } } #search-hub-toolbar { position: fixed !important; background: var(--bg-color) !important; border-radius: 12px !important; padding: 8px !important; display: flex !important; gap: 8px !important; z-index: 2147483647 !important; max-width: 90vw !important; overflow-x: auto !important; scrollbar-width: none !important; box-shadow: 0 -2px 8px rgba(0,0,0,0.1) !important; touch-action: pan-x !important; user-select: none !important; -webkit-user-select: none !important; pointer-events: auto !important; transition: all ${CONFIG.ANIMATION_MS}ms ease; } #search-hub-toolbar.dragging { background: var(--dragging-bg) !important; transform: translate(var(--drag-x, 0), var(--drag-y, 0)) !important; transition: none !important; } #search-hub-toolbar::-webkit-scrollbar { display: none !important; } #search-hub-toolbar[data-position="top"] { top: 0 !important; bottom: auto !important; left: 50% !important; right: auto !important; transform: translateX(-50%) !important; border-radius: 0 0 12px 12px !important; flex-direction: row !important; overflow-x: auto !important; overflow-y: hidden !important; } #search-hub-toolbar[data-position="bottom"] { bottom: 0 !important; top: auto !important; left: 50% !important; right: auto !important; transform: translateX(-50%) !important; border-radius: 12px 12px 0 0 !important; flex-direction: row !important; overflow-x: auto !important; overflow-y: hidden !important; } #search-hub-toolbar[data-position="left"] { top: 50% !important; bottom: auto !important; left: 0 !important; right: auto !important; transform: translateY(-50%) !important; flex-direction: column !important; border-radius: 0 12px 12px 0 !important; max-height: 90vh !important; overflow-x: hidden !important; overflow-y: auto !important; touch-action: pan-y !important; } #search-hub-toolbar[data-position="right"] { top: 50% !important; bottom: auto !important; right: 0 !important; left: auto !important; transform: translateY(-50%) !important; flex-direction: column !important; border-radius: 12px 0 0 12px !important; max-height: 90vh !important; overflow-x: hidden !important; overflow-y: auto !important; touch-action: pan-y !important; } .engine-btn { padding: 6px 12px !important; background: var(--btn-bg) !important; color: var(--text-color) !important; border: 0.8px solid var(--border-color) !important; border-radius: 8px !important; cursor: pointer !important; transition: background 0.2s ease !important; white-space: nowrap !important; box-sizing: border-box !important; } .engine-btn:hover { background: var(--hover-bg) !important; } .settings-btn { padding: 6px 12px !important; background: var(--btn-bg) !important; color: var(--text-color) !important; border: 0.8px solid var(--border-color) !important; border-radius: 8px !important; cursor: pointer !important; box-sizing: border-box !important; } @media (max-width: 640px) { #search-hub-toolbar { max-width: 95vw !important; padding: 6px !important; } .engine-btn, .settings-btn { padding: 4px 8px !important; } } `; // 设置面板 CSS const SETTINGS_CSS = ` :host { all: initial; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important; font-weight: normal !important; font-size: 14px !important; --panel-bg: white; --text-color: #1f2937; --border-color: #e5e7eb; --hover-bg: #f9fafb; --btn-bg: #f9fafb; --btn-active-bg: #e5e7eb; --btn-save-bg: #4f46e5; --btn-add-bg: #22c55e; --btn-close-bg: #6b7280; } @media (prefers-color-scheme: dark) { :host { --panel-bg: #1f2937; --text-color: #e5e7eb; --border-color: #4b5563; --hover-bg: #374151; --btn-bg: #374151; --btn-active-bg: #4b5563; } } @keyframes fadeIn { from { opacity: 0; transform: translate(-50%, -50%) scale(0.95); } to { opacity: 1; transform: translate(-50%, -50%) scale(1); } } .settings-panel { position: fixed !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%) !important; background: var(--panel-bg) !important; border-radius: 12px !important; padding: 16px !important; box-shadow: 0 4px 16px rgba(0,0,0,0.2) !important; z-index: 2147483647 !important; width: 50vw !important; min-width: 300px !important; max-width: 800px !important; max-height: 80vh !important; overflow-y: auto !important; box-sizing: border-box !important; animation: fadeIn ${CONFIG.ANIMATION_MS}ms ease forwards !important; pointer-events: auto !important; color: var(--text-color) !important; } h3 { font-size: 16px !important; font-weight: normal !important; margin: 0 0 12px !important; padding-bottom: 8px !important; border-bottom: 1px solid var(--border-color) !important; } .engine-item { margin-bottom: 12px !important; border: 1px solid var(--border-color) !important; border-radius: 6px !important; padding: 0 !important; } .name-row { display: flex !important; gap: 8px !important; align-items: center !important; margin: 8px !important; } .name-row input { flex: 1 !important; padding: 6px 8px !important; border: 1px solid var(--border-color) !important; border-radius: 4px !important; box-sizing: border-box !important; background: var(--panel-bg) !important; color: var(--text-color) !important; } .url-input { width: calc(100% - 16px) !important; margin: 0 8px 8px !important; padding: 6px 8px !important; border: 1px solid var(--border-color) !important; border-radius: 4px !important; box-sizing: border-box !important; background: var(--panel-bg) !important; color: var(--text-color) !important; } .actions { display: flex !important; gap: 4px !important; } .action-btn { width: 24px !important; height: 24px !important; padding: 0 !important; border: 1px solid var(--border-color) !important; border-radius: 4px !important; background: var(--btn-bg) !important; cursor: pointer !important; display: flex !important; align-items: center !important; justify-content: center !important; box-sizing: border-box !important; color: var(--text-color) !important; } .action-btn:hover { background: var(--hover-bg) !important; } .action-btn:disabled { opacity: 0.5 !important; cursor: not-allowed !important; } .panel-actions { display: flex !important; gap: 8px !important; margin-top: 12px !important; border-top: 1px solid var(--border-color) !important; padding-top: 12px !important; justify-content: flex-end !important; } .panel-btn { padding: 8px 16px !important; border-radius: 6px !important; border: none !important; cursor: pointer !important; box-sizing: border-box !important; color: white !important; } .add-btn { background: var(--btn-add-bg) !important; } .save-btn { background: var(--btn-save-bg) !important; } .close-btn { background: var(--btn-close-bg) !important; } @media (max-width: 640px) { .settings-panel { width: 50vw !important; min-width: 280px !important; } .name-row input { max-width: calc(100% - 94px) !important; } .url-input { width: calc(100% - 16px) !important; } .panel-btn { padding: 6px 12px !important; } } `; // 存储工具函数 const safeGetStorage = (key, defaultValue) => { try { return GM_getValue(key, defaultValue); } catch (e) { console.error(`GM_getValue failed for ${key}:`, e); return defaultValue; } }; const safeSetStorage = (key, value) => { try { GM_setValue(key, value); return true; } catch (e) { console.error(`GM_setValue failed for ${key}:`, e); return false; } }; // 工具函数 const decode = str => str ? decodeURIComponent(str) : ''; const buildSearchUrl = (protocol, hostname, basePath, queryParam, extraParams = '') => { let urlPath = basePath; let queryString = ''; if (queryParam) { const separator = basePath === '/' ? '?' : basePath.includes('?') ? '&' : '?'; queryString = `${separator}${queryParam}=%s`; } else { urlPath = basePath.endsWith('/') ? `${basePath}%s/` : `${basePath}/%s`; } return `${protocol}//${hostname}${urlPath}${queryString}${extraParams}`; }; const debounce = (fn, ms) => { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => fn(...args), ms); }; }; const throttle = (fn, ms) => { let last = 0; return (...args) => { const now = Date.now(); if (now - last > ms) { last = now; fn(...args); } }; }; const sanitize = str => { if (typeof str !== 'string') return ''; return str.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": "'" })[c]); }; const generateId = () => `se_${Math.random().toString(36).slice(2, 10)}`; function getEngineConfigFromCurrentPage() { if (!SearchDetector.isSearchPage()) { alert(i18n[lang].alertNotSearchPage); return null; } const engineConfig = SearchDetector.detectEngineConfig(); if (!engineConfig) { alert(i18n[lang].alertNoEngineConfig); return null; } return engineConfig; } // 搜索页面检测 class SearchDetector { static cachedInput = null; static cachedSearchPage = null; static cachedUrl = null; static cachedForm = null; static pexelsPathRegex = /^\/[a-z]{2}(-[a-z]{2})?\/search\//; static inputSelector = 'input[type="search"], input[name="q"], input[name="wd"], input[name="word"], input[name="search"], input[name="query"], input[name="text"], input[name="p"], input[name="i"], input[name="searchword"], input[name="lookfor"], input.search-input'; static config = { domains: { 'metaso.cn': { basePath: '/', queryParam: 'q', displayName: 'Metaso' }, 'www.baidu.com': { basePath: '/s', queryParam: 'wd', displayName: 'Baidu' }, 'm.baidu.com': { basePath: '/s', queryParam: 'word', displayName: 'Baidu' }, 'www.yandex.com': { basePath: '/search', queryParam: 'text', displayName: 'Yandex' }, 'search.yahoo.com': { basePath: '/search', queryParam: 'p', displayName: 'Yahoo' }, 'www.startpage.com': { basePath: '/search', queryParam: 'q', displayName: 'Startpage' }, 'search.aol.com': { basePath: '/aol/search', queryParam: 'q', displayName: 'AOL' }, 'www.sogou.com': { basePath: '/web', queryParam: 'query', displayName: 'Sogou' }, 'm.sogou.com': { basePath: '/web/searchList.jsp', queryParam: 'keyword', displayName: 'Sogou' }, 'm.weibo.cn': { basePath: '/search', queryParam: 'q', extraParams: `containerid=${CONFIG.WEIBO_CONTAINER_ID}`, displayName: 'Weibo' }, 'zh.m.wikipedia.org': { basePath: '/wiki', queryParam: null, displayName: 'Wikipedia' }, 'www.pexels.com': { basePath: '/zh-cn/search', queryParam: null, displayName: 'Pexels' }, 'www.wolframalpha.com': { basePath: '/input', queryParam: 'i', displayName: 'WolframAlpha' }, 'i.cnki.net': { basePath: '/searchResult.html', queryParam: 'searchword', displayName: 'CNKI' }, 'www.base-search.net': { basePath: '/Search/Results', queryParam: 'lookfor', displayName: 'BASE' } }, exclude: [ { domain: /baidu\.com$/, paths: [/^\/(tieba|zhidao|question|passport)/] }, ], commonQueryParams: ['q', 'wd', 'word', 'keyword', 'search', 'query', 'text', 'p', 'i', 'searchword', 'lookfor'], }; static init() { this.observeSearchInput(); } static observeSearchInput() { const clearCache = throttle(() => { this.cachedInput = null; this.cachedForm = null; this.getSearchInput(); if (DEBUG) console.log('Search input cache cleared'); }, 100); const observer = new MutationObserver(mutations => { const isSearchRelated = mutations.some(mutation => Array.from(mutation.addedNodes).some(node => node.nodeType === 1 && (node.matches('form') || node.querySelector(this.inputSelector)) ) || Array.from(mutation.removedNodes).some(node => node.nodeType === 1 && (node.matches('form') || node.querySelector(this.inputSelector)) ) ); if (isSearchRelated) { clearCache(); } }); const target = document.querySelector('form') || document.querySelector('main') || document.body; observer.observe(target, { childList: true, subtree: false }); if (DEBUG) console.log('Search input observer initialized on:', target.tagName); } static isSearchPage() { const start = performance.now(); if (location.href === this.cachedUrl && this.cachedSearchPage !== null) { if (DEBUG) console.log('Using cached search page result:', this.cachedSearchPage); return this.cachedSearchPage; } try { const url = new URL(location.href); const params = new URLSearchParams(url.search); if (this.config.commonQueryParams.some(param => params.has(param) && params.get(param).trim())) { if (DEBUG) console.log('Fast path: Detected search page via URL params'); this.cachedSearchPage = true; this.cachedUrl = location.href; return true; } if (this.isPredefinedDomain(url, params)) { if (DEBUG) console.log('Detected predefined search page:', url.hostname); this.cachedSearchPage = true; this.cachedUrl = location.href; return true; } if (this.isCustomEngineMatch(url, params)) { if (DEBUG) console.log('Detected custom engine page:', location.href); this.cachedSearchPage = true; this.cachedUrl = location.href; return true; } if (this.isExcludedPage(url)) { if (DEBUG) console.log('Page excluded:', location.href); this.cachedSearchPage = false; this.cachedUrl = location.href; return false; } const hasSearchInput = !!this.getSearchInput()?.value?.trim(); const hasSearchTitle = document.title.toLowerCase().includes('search') || document.title.includes('搜索'); const result = hasSearchInput || hasSearchTitle; this.cachedSearchPage = result; this.cachedUrl = location.href; if (DEBUG) console.log('Fallback detection:', { hasSearchInput, hasSearchTitle, url: location.href }); return result; } catch (e) { console.error('SearchDetector.isSearchPage error:', e); this.cachedSearchPage = false; this.cachedUrl = location.href; return false; } finally { if (DEBUG) console.log(`isSearchPage took: ${performance.now() - start}ms`); } } static isGenericSearchPage(url, params) { return ['/search', '/s', '/web', '/results'].some(path => url.pathname.toLowerCase().includes(path)) && this.config.commonQueryParams.some(param => params.has(param) && params.get(param).trim()); } static isPredefinedDomain(url, params) { const domainConfig = this.config.domains[url.hostname]; if (!domainConfig) return false; if (url.hostname === 'zh.m.wikipedia.org' && url.pathname.startsWith('/wiki/')) return true; if (url.hostname === 'm.weibo.cn' && url.pathname === '/search' && params.get('containerid') === CONFIG.WEIBO_CONTAINER_ID) return true; if (url.hostname === 'www.pexels.com' && url.pathname.match(this.pexelsPathRegex)) return true; return domainConfig.basePath === url.pathname.split('?')[0]; } static isCustomEngineMatch(url, params) { const customEngines = safeGetStorage(CONFIG.STORAGE_KEY, []); for (const engine of customEngines) { try { const engineUrl = new URL(engine.url.replace('%s', 'test')); if (engineUrl.hostname === url.hostname) { const customConfig = this.custom(engine); if (customConfig && customConfig.pathTest.test(url.pathname)) { return true; } } } catch (e) { console.warn('Error checking custom engine:', engine, e); } } return false; } static isExcludedPage(url) { for (const rule of this.config.exclude) { if (rule.domain.test(url.hostname) && rule.paths.some(ex => ex.test(url.pathname))) { return true; } } return false; } static getSearchInput() { if (this.cachedInput !== null) return this.cachedInput; if (!this.cachedForm) { this.cachedForm = document.querySelector('form'); } if (this.cachedForm) { this.cachedInput = this.cachedForm.querySelector(this.inputSelector); if (this.cachedInput) return this.cachedInput; } this.cachedInput = document.querySelector(this.inputSelector); return this.cachedInput; } static getQuery() { try { const url = new URL(location.href); const params = new URLSearchParams(url.search); for (const param of this.config.commonQueryParams) { const value = params.get(param)?.trim(); if (value) { if (DEBUG) console.log('Query extracted from params:', decode(value)); return decode(value); } } const inputValue = this.getSearchInput()?.value?.trim(); if (inputValue) { if (DEBUG) console.log('Query extracted from input:', inputValue); return inputValue; } if (url.hostname === 'zh.m.wikipedia.org' && url.pathname.startsWith('/wiki/')) { const title = url.pathname.replace('/wiki/', '').split('/')[0]; if (DEBUG) console.log('Query extracted from Wikipedia path:', decode(title)); return decode(title); } if (url.hostname === 'www.pexels.com' && url.pathname.match(this.pexelsPathRegex)) { const query = url.pathname.split(this.pexelsPathRegex)[1]?.replace(/\/$/, ''); if (DEBUG) console.log('Query extracted from Pexels path:', decode(query)); return decode(query); } const metaQuery = document.querySelector('meta[name="description"]')?.content?.replace(/.*?search\s*for\s*['"]([^'"]+)['"].*/i, '$1')?.trim(); if (metaQuery) { if (DEBUG) console.log('Query extracted from meta:', metaQuery); return metaQuery; } const titleQuery = document.title.replace(/\s*[-_|](搜索|Search|Query|Results).*$/i, '').trim(); if (titleQuery) { if (DEBUG) console.log('Query extracted from title:', titleQuery); return titleQuery; } console.warn('Failed to extract query:', location.href); return ''; } catch (e) { console.error('SearchDetector.getQuery error:', e); return ''; } } static custom(e) { try { const testQuery = 'testquery'; const u = new URL(e.url.replace('%s', testQuery)); let paramKeys = []; new URLSearchParams(u.search).forEach((v, key) => { if (v === testQuery) paramKeys.push(key); }); if (!paramKeys.length && e.url.includes('%s')) { console.warn('No query param detected, defaulting to "q"'); paramKeys = ['q']; } const pathTest = new RegExp(`^${u.pathname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(/.*)?$`); if (!u.hostname) { console.error('Invalid hostname in URL:', e.url); return null; } return { pathTest, paramKeys }; } catch (err) { console.error('SearchDetector.custom error:', err); return null; } } static getQueryParam(forms, params, commonQueryParams) { const searchForm = Array.from(forms).find(form => form.querySelector(this.inputSelector)); if (searchForm) { const searchInput = searchForm.querySelector(this.inputSelector); if (searchInput && searchInput.name) { return { queryParam: searchInput.name, source: 'form-input' }; } try { const actionUrl = new URL(searchForm.action, location.origin); const actionParams = new URLSearchParams(actionUrl.search); for (const param of commonQueryParams) { if (actionParams.has(param)) { return { queryParam: param, source: 'form-action' }; } } } catch (e) { console.warn('Form action parsing failed:', e); } } for (const param of commonQueryParams) { if (params.has(param) && params.get(param).trim()) { return { queryParam: param, source: 'url-param' }; } } if (this.getSearchInput()?.value?.trim()) { return { queryParam: 'q', source: 'input-fallback' }; } return null; } static detectEngineConfig() { try { const url = new URL(location.href); const params = new URLSearchParams(url.search); const forms = document.querySelectorAll('form[action]'); const domainConfig = this.config.domains[url.hostname]; if (domainConfig) { if (url.hostname === 'zh.m.wikipedia.org') { return { name: domainConfig.displayName, url: buildSearchUrl(url.protocol, url.hostname, '/wiki', null) }; } if (url.hostname === 'm.weibo.cn') { return { name: domainConfig.displayName, url: buildSearchUrl(url.protocol, url.hostname, domainConfig.basePath, domainConfig.queryParam, `&${domainConfig.extraParams}`) }; } if (url.hostname === 'www.pexels.com') { return { name: domainConfig.displayName, url: buildSearchUrl(url.protocol, url.hostname, '/zh-cn/search', null) }; } let extraParams = ''; if (url.hostname === 'www.base-search.net' && params.has('language')) { extraParams = `&language=${params.get('language')}`; } return { name: domainConfig.displayName, url: buildSearchUrl(url.protocol, url.hostname, domainConfig.basePath, domainConfig.queryParam, extraParams) }; } const queryResult = this.getQueryParam(forms, params, this.config.commonQueryParams); if (!queryResult) { console.warn('Failed to extract query param:', location.href); return null; } const { queryParam, source } = queryResult; let basePath = this.getBasePath(url, forms, source); return this.buildEngineConfig(url, basePath, queryParam); } catch (e) { console.error('SearchDetector.detectEngineConfig error:', e); return null; } } static getBasePath(url, forms, source) { if (source.startsWith('form')) { const searchForm = Array.from(forms).find(form => form.querySelector(this.inputSelector)); if (searchForm) { try { const actionUrl = new URL(searchForm.action, url.origin); return actionUrl.pathname || '/'; } catch (e) { console.warn('Form action parsing failed:', e); } } } const pathSegments = url.pathname.split('/').filter(segment => segment); const staticSegments = pathSegments.filter(segment => !/^[0-9]+$/.test(segment) && !/^from=/.test(segment) && !/^ssid=/.test(segment) && segment.length < 20 ); return staticSegments.length > 0 ? `/${staticSegments.join('/')}` : '/'; } static buildEngineConfig(url, basePath, queryParam) { const hostnameParts = url.hostname.split('.'); const commonSubdomains = ['www', 'm', 'mobile', 'search']; const tlds = ['com', 'cn', 'org', 'net', 'co', 'io', 'site', 'ai']; const significantParts = hostnameParts.filter(part => !commonSubdomains.includes(part) && !tlds.includes(part)); const engineName = significantParts.length > 0 ? significantParts[significantParts.length - 1] : hostnameParts[0]; const displayName = engineName.charAt(0).toUpperCase() + engineName.slice(1); return { name: displayName, url: buildSearchUrl(url.protocol, url.hostname, basePath, queryParam) }; } } // 设置面板 class SettingsPanel { constructor(searchHub) { this.searchHub = searchHub; this.panel = null; } render() { try { this.panel = document.createElement('div'); this.panel.id = 'settings-panel-container'; this.panel.setAttribute('translate', 'no'); const shadow = this.panel.attachShadow({ mode: 'open' }); const style = document.createElement('style'); setTimeout(() => { style.textContent = SETTINGS_CSS; if (DEBUG) console.log('Settings panel CSS loaded'); }, 0); const content = document.createElement('div'); content.className = 'settings-panel'; content.innerHTML = `