// ==UserScript== // @name Chrome 商店链接自动替换为镜像 // @name:en Chrome Web Store Link to Mirror // @namespace https://github.com/userscripts/chrome-store-mirror // @version 1.0.1 // @description 自动将网页上的 Chrome 网上应用店链接替换为 CrxSoso 镜像链接,并提供关键词快速搜索功能 // @description:en Automatically replace Chrome Web Store links with CrxSoso mirror links, with quick search menu // @author You // @match *://*/* // @grant GM_registerMenuCommand // @grant GM_openInTab // @run-at document-idle // @license AI生成,随意使用 // ==/UserScript== (function () { 'use strict'; // ========== 配置 ========== const CONFIG = { // 镜像站地址,可根据需要修改 mirrorBase: 'https://www.crxsoso.com/webstore/detail/', // 镜像站搜索页地址 searchBase: 'https://www.crxsoso.com/search', // Chrome 商店域名列表 storeDomains: [ 'chromewebstore.google.com', 'chrome.google.com' ], // 特征码长度(Chrome 扩展 ID 固定为 32 位小写字母) extensionIdLength: 32, // 是否在控制台输出日志 debug: false, // 搜索结果是否在新标签页打开 searchOpenInNewTab: true }; // ========== 工具函数 ========== /** * 日志输出 */ function log(...args) { if (CONFIG.debug) { console.log('[ChromeStoreMirror]', ...args); } } /** * 判断 URL 是否为 Chrome 商店链接 */ function isChromeStoreUrl(url) { try { const u = new URL(url); return CONFIG.storeDomains.some(domain => u.hostname === domain); } catch { return false; } } /** * 从 Chrome 商店链接中提取扩展 ID(特征码) * 支持格式: * https://chromewebstore.google.com/detail/{name}/{extId} * https://chromewebstore.google.com/detail/{extId} * https://chrome.google.com/webstore/detail/{name}/{extId} * https://chrome.google.com/webstore/detail/{extId} * 带 query/hash 的版本也能处理 */ function extractExtensionId(url) { try { const u = new URL(url); // 去掉 query 和 hash,只看 pathname const pathname = u.pathname; // Chrome 扩展 ID 正则:32 位小写字母(a-p) const idRegex = /([a-p]{32})/; const match = pathname.match(idRegex); if (match) { return match[1]; } return null; } catch { return null; } } /** * 根据扩展 ID 生成镜像链接 */ function buildMirrorUrl(extensionId) { return CONFIG.mirrorBase + extensionId; } /** * 根据关键词生成镜像搜索页链接 */ function buildSearchUrl(keyword) { const url = new URL(CONFIG.searchBase); url.searchParams.set('keyword', keyword); url.searchParams.set('store', 'chrome'); return url.href; } /** * 统一的页面跳转函数(优先使用 GM_openInTab,兼容新窗口/当前窗口) */ function openUrl(url, newTab) { const openInNew = newTab !== undefined ? newTab : CONFIG.searchOpenInNewTab; try { if (openInNew && typeof GM_openInTab === 'function') { GM_openInTab(url, { active: true }); } else if (openInNew) { window.open(url, '_blank'); } else { location.href = url; } } catch (err) { // 回退到 window.open if (openInNew) { window.open(url, '_blank'); } else { location.href = url; } } } /** * 处理单个链接元素 * 返回是否进行了替换 */ function processLinkElement(a) { // 已经处理过的跳过 if (a.dataset._chromeStoreMirrorProcessed === 'true') { return false; } a.dataset._chromeStoreMirrorProcessed = 'true'; const href = a.getAttribute('href'); if (!href) return false; // 处理相对/绝对 URL let absoluteUrl; try { absoluteUrl = new URL(href, location.href).href; } catch { return false; } if (!isChromeStoreUrl(absoluteUrl)) return false; const extId = extractExtensionId(absoluteUrl); if (!extId) { log('未找到扩展 ID:', href); return false; } const mirrorUrl = buildMirrorUrl(extId); // 替换 href a.setAttribute('href', mirrorUrl); // 替换文本内容(如果文本看起来像是原链接) const text = a.textContent.trim(); if (text && isChromeStoreUrl(text)) { a.textContent = mirrorUrl; } // 添加提示 tooltip const oldTitle = a.getAttribute('title') || ''; if (!oldTitle.includes('镜像')) { a.setAttribute( 'title', (oldTitle ? oldTitle + ' | ' : '') + '已替换为 CrxSoso 镜像链接' ); } // 添加样式标记 a.style.setProperty('border-bottom', '1px dashed #4caf50'); a.style.setProperty('text-decoration-color', '#4caf50'); log('替换链接:', href, '->', mirrorUrl); return true; } /** * 批量处理容器内的所有链接 */ function processLinksIn(container) { if (!container) return 0; let count = 0; // 如果容器本身就是 a 标签 if (container.tagName && container.tagName.toLowerCase() === 'a') { if (processLinkElement(container)) count++; } // 查找所有子 a 标签 const links = container.querySelectorAll && container.querySelectorAll('a[href]'); if (links) { links.forEach(a => { if (processLinkElement(a)) count++; }); } return count; } // ========== 点击拦截(双重保障,防止某些动态绑定的事件绕过 href) ========== /** * 在捕获阶段拦截点击事件,检查是否是 Chrome 商店链接 */ document.addEventListener('click', function (e) { const a = e.target.closest && e.target.closest('a[href]'); if (!a) return; const href = a.getAttribute('href'); if (!href) return; let absoluteUrl; try { absoluteUrl = new URL(href, location.href).href; } catch { return; } if (!isChromeStoreUrl(absoluteUrl)) return; const extId = extractExtensionId(absoluteUrl); if (!extId) return; // 链接还没被替换的情况下,强制跳转 if (a.dataset._chromeStoreMirrorProcessed !== 'true' || !a.getAttribute('href').startsWith(CONFIG.mirrorBase)) { e.preventDefault(); e.stopPropagation(); const mirrorUrl = buildMirrorUrl(extId); // 处理 target const target = a.getAttribute('target') || '_self'; if (target === '_blank') { window.open(mirrorUrl, '_blank'); } else { location.href = mirrorUrl; } log('点击拦截跳转:', absoluteUrl, '->', mirrorUrl); } }, true); // 捕获阶段,先于页面自身的 click handler 执行 // ========== DOM 变化监听(处理动态加载的内容) ========== // 节流,避免短时间内大量触发 let observerTimer = null; function scheduleProcess(root) { if (observerTimer) return; observerTimer = setTimeout(() => { observerTimer = null; const count = processLinksIn(root || document.body); if (count > 0) { log('DOM 变化,处理了', count, '个新链接'); } }, 100); } const observer = new MutationObserver(function (mutations) { for (const m of mutations) { if (m.type === 'childList' && m.addedNodes.length > 0) { scheduleProcess(document.body); break; } // 处理属性变化(href 被页面脚本动态改掉) if (m.type === 'attributes' && m.attributeName === 'href') { const target = m.target; if (target.tagName && target.tagName.toLowerCase() === 'a') { // 重置处理标记,让它重新被处理 delete target.dataset._chromeStoreMirrorProcessed; scheduleProcess(target); } } } }); // ========== 搜索窗口 + 油猴菜单 ========== // 搜索弹窗 DOM 引用 let searchDialog = null; /** * 从剪贴板读取文本(带回退) */ function readClipboardFallback() { return new Promise((resolve) => { if (navigator.clipboard && navigator.clipboard.readText) { navigator.clipboard.readText().then(resolve).catch(() => resolve('')); } else { resolve(''); } }); } /** * 检查字符串是否是合法的 Chrome 扩展 ID */ function isExtensionId(str) { return /^[a-p]{32}$/.test(str.trim()); } /** * 创建并显示搜索弹窗 */ async function openSearchDialog() { // 已存在则直接显示 if (searchDialog && document.body.contains(searchDialog)) { searchDialog.style.display = 'flex'; const input = searchDialog.querySelector('#csm-search-input'); if (input) { input.focus(); input.select(); } return; } // 注入样式(只注入一次) if (!document.getElementById('csm-search-style')) { const style = document.createElement('style'); style.id = 'csm-search-style'; style.textContent = ` #csm-search-mask{ position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:2147483646; display:flex;align-items:center;justify-content:center; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif; animation:csm-fadein .15s ease-out; } @keyframes csm-fadein{from{opacity:0}to{opacity:1}} @keyframes csm-slideup{from{transform:translateY(-20px);opacity:0}to{transform:translateY(0);opacity:1}} #csm-search-dialog{ width:min(560px,92vw);background:#fff;border-radius:12px; box-shadow:0 20px 60px rgba(0,0,0,.3);padding:24px; animation:csm-slideup .2s ease-out;box-sizing:border-box; } #csm-search-title{ margin:0 0 16px;font-size:18px;font-weight:600;color:#1a1a1a; display:flex;align-items:center;gap:8px; } #csm-search-title::before{ content:"";display:inline-block;width:20px;height:20px;border-radius:5px; background:linear-gradient(135deg,#4caf50,#2196f3); } #csm-search-input-wrap{position:relative;} #csm-search-input{ width:100%;padding:12px 44px 12px 14px;box-sizing:border-box; border:2px solid #e0e0e0;border-radius:8px;font-size:15px; outline:none;transition:border-color .15s,box-shadow .15s;color:#1a1a1a; background:#fafafa; } #csm-search-input:focus{ border-color:#4caf50;background:#fff; box-shadow:0 0 0 3px rgba(76,175,80,.15); } #csm-search-clear{ position:absolute;right:10px;top:50%;transform:translateY(-50%); width:28px;height:28px;border:none;background:transparent;cursor:pointer; border-radius:50%;display:none;align-items:center;justify-content:center; color:#999;font-size:18px;line-height:1; } #csm-search-clear:hover{background:#eee;color:#333;} #csm-search-tip{ margin-top:10px;font-size:12px;color:#666;line-height:1.6; } #csm-search-tip code{ background:#f0f0f0;padding:1px 5px;border-radius:4px;font-size:11px; color:#d63384; } #csm-search-btns{ margin-top:18px;display:flex;gap:10px;justify-content:flex-end; } .csm-btn{ padding:8px 18px;border-radius:6px;border:none;cursor:pointer; font-size:14px;font-weight:500;transition:all .15s; } #csm-btn-cancel{background:#f0f0f0;color:#555;} #csm-btn-cancel:hover{background:#e0e0e0;} #csm-btn-search{ background:linear-gradient(135deg,#4caf50,#43a047);color:#fff; box-shadow:0 2px 6px rgba(76,175,80,.35); } #csm-btn-search:hover{ box-shadow:0 4px 10px rgba(76,175,80,.45);transform:translateY(-1px); } #csm-btn-search:disabled{opacity:.5;cursor:not-allowed;transform:none;} `; document.head.appendChild(style); } // 创建 DOM const mask = document.createElement('div'); mask.id = 'csm-search-mask'; mask.innerHTML = ` `; searchDialog = mask; document.body.appendChild(mask); const input = mask.querySelector('#csm-search-input'); const clearBtn = mask.querySelector('#csm-search-clear'); const cancelBtn = mask.querySelector('#csm-btn-cancel'); const searchBtn = mask.querySelector('#csm-btn-search'); // 尝试读取剪贴板作为默认值(仅当内容像是扩展 ID 或 Chrome 商店链接) try { const clipText = await readClipboardFallback(); if (clipText) { const trimmed = clipText.trim(); if (isExtensionId(trimmed)) { input.value = trimmed; } else if (isChromeStoreUrl(trimmed)) { const extId = extractExtensionId(trimmed); if (extId) input.value = extId; } } } catch {} // 输入内容变化 → 控制清除按钮显示 input.addEventListener('input', () => { clearBtn.style.display = input.value ? 'flex' : 'none'; }); clearBtn.addEventListener('click', () => { input.value = ''; clearBtn.style.display = 'none'; input.focus(); }); if (input.value) clearBtn.style.display = 'flex'; // 关闭弹窗 function closeDialog() { mask.style.display = 'none'; } // 执行搜索 function doSearch(forceCurrentTab) { const keyword = input.value.trim(); if (!keyword) { input.focus(); input.style.animation = 'none'; // 抖动提示 input.offsetHeight; input.animate( [ { transform: 'translateX(0)' }, { transform: 'translateX(-6px)' }, { transform: 'translateX(6px)' }, { transform: 'translateX(-3px)' }, { transform: 'translateX(3px)' }, { transform: 'translateX(0)' } ], { duration: 250 } ); return; } searchBtn.disabled = true; const url = buildSearchUrl(keyword); log('搜索跳转:', keyword, '->', url); openUrl(url, forceCurrentTab ? false : undefined); setTimeout(closeDialog, 80); } // 按钮事件 cancelBtn.addEventListener('click', closeDialog); searchBtn.addEventListener('click', () => doSearch(false)); // 键盘事件 mask.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.preventDefault(); closeDialog(); } else if (e.key === 'Enter') { e.preventDefault(); // Ctrl+Enter 当前页,Enter 新标签页 doSearch(e.ctrlKey || e.metaKey); } }, true); // 点击遮罩关闭(但不关闭对话框本体) mask.addEventListener('mousedown', (e) => { if (e.target === mask) closeDialog(); }); // 聚焦输入框 setTimeout(() => { input.focus(); input.select(); }, 10); } /** * 注册油猴菜单命令(兼容不同脚本管理器) */ function registerMenuCommands() { const commands = [ { name: '🔍 搜索扩展 / 关键词', func: openSearchDialog, accessKey: 'F' }, { name: '⚙️ 打开 CrxSoso 首页', func: () => openUrl('https://www.crxsoso.com/', true), accessKey: 'H' } ]; let registered = 0; try { if (typeof GM_registerMenuCommand === 'function') { commands.forEach(cmd => { try { GM_registerMenuCommand(cmd.name, cmd.func, cmd.accessKey); registered++; } catch (e) { log('菜单注册失败:', cmd.name, e); } }); } else if (typeof GM === 'object' && typeof GM.registerMenuCommand === 'function') { commands.forEach(cmd => { try { GM.registerMenuCommand(cmd.name, cmd.func, cmd.accessKey); registered++; } catch (e) { log('菜单注册失败:', cmd.name, e); } }); } } catch (e) { log('油猴菜单注册跳过:', e); } if (registered === 0) { // 兼容方案:注入页面右上角悬浮按钮 registerFloatingButton(); } } /** * 降级方案:悬浮按钮(当 GM_registerMenuCommand 不可用时) */ function registerFloatingButton() { if (document.getElementById('csm-float-btn')) return; const btn = document.createElement('button'); btn.id = 'csm-float-btn'; btn.type = 'button'; btn.title = 'CrxSoso 搜索(点击打开)'; btn.textContent = '🔍'; Object.assign(btn.style, { position: 'fixed', right: '18px', bottom: '80px', zIndex: '2147483645', width: '46px', height: '46px', borderRadius: '50%', border: 'none', cursor: 'pointer', fontSize: '20px', background: 'linear-gradient(135deg,#4caf50,#2196f3)', color: '#fff', boxShadow: '0 4px 14px rgba(0,0,0,.25)', display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'transform .15s, box-shadow .15s' }); btn.addEventListener('mouseenter', () => { btn.style.transform = 'scale(1.08)'; btn.style.boxShadow = '0 6px 20px rgba(0,0,0,.3)'; }); btn.addEventListener('mouseleave', () => { btn.style.transform = 'scale(1)'; btn.style.boxShadow = '0 4px 14px rgba(0,0,0,.25)'; }); btn.addEventListener('click', openSearchDialog); document.body.appendChild(btn); } // ========== 初始化 ========== function init() { // 注册油猴菜单(或降级为悬浮按钮) registerMenuCommands(); // 处理初始页面 const initialCount = processLinksIn(document.body); log('初始页面处理完成,替换了', initialCount, '个链接'); // 启动 DOM 监听 observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['href'] }); log('脚本已启动'); } // 有些页面 document-idle 时 body 还没准备好,多做一次检查 if (document.body) { init(); } else { document.addEventListener('DOMContentLoaded', init); } })();