// ==UserScript== // @name ALook浏览器脚本直装助手(GM) // @namespace https://www.alookweb.com/ // @version 1.60 // @description 还原ALook原生安装协议识别并安装user.js后缀的脚本,模拟了一些简单的GM函数,并支持通过菜单直链或本地文件安装脚本。 // @author Deepseek // @match *://*/* // @icon https://www.alookweb.com/index_files/alook.png // @grant none // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; const SCRIPT_ID_SALT = 'alook-userscript:v1:'; const INSTALLED_KEY_PREFIX = 'alook-userscript-installed:v1:'; function normalizeScriptName(name) { let s = String(name || '未命名脚本').trim(); try { s = s.normalize('NFKC'); } catch (_) {} return s.replace(/\s+/g, ' ').toLowerCase(); } function fnv1a64(str) { let h = 0xcbf29ce484222325n; const prime = 0x100000001b3n; const mask = 0xffffffffffffffffn; for (let i = 0; i < str.length; i++) { h ^= BigInt(str.charCodeAt(i)); h = (h * prime) & mask; } return h.toString(16).padStart(16, '0'); } function makeScriptId(stableName, namespace, matchKey) { const ns = String(namespace || '').trim(); const mk = String(matchKey || '').trim(); const basis = ns ? (ns + '\n' + normalizeScriptName(stableName)) : (normalizeScriptName(stableName) + '\n' + mk); return `userscript-${fnv1a64(SCRIPT_ID_SALT + basis)}`; } function getBrowserLanguages() { const list = []; try { if (navigator.languages && navigator.languages.length) { for (let i = 0; i < navigator.languages.length; i++) list.push(navigator.languages[i]); } } catch (_) {} try { if (navigator.language) list.push(navigator.language); } catch (_) {} try { if (navigator.userLanguage) list.push(navigator.userLanguage); } catch (_) {} const out = []; for (let i = 0; i < list.length; i++) { const v = String(list[i] || '').trim(); if (v && out.indexOf(v) === -1) out.push(v); } return out; } function pickScriptName(meta) { const names = {}; for (const key in meta) { if (key === 'name' || key.indexOf('name:') === 0) { const list = meta[key]; if (list && list.length) names[key] = list[0]; } } const langs = getBrowserLanguages(); for (let i = 0; i < langs.length; i++) { const lower = langs[i].toLowerCase(); const candidates = [lower]; const short = lower.split('-')[0]; if (short && short !== lower) candidates.push(short); for (let j = 0; j < candidates.length; j++) { const key = 'name:' + candidates[j]; if (names[key]) return names[key]; } } const fallbackOrder = ['name:zh-cn', 'name:zh-tw', 'name:en', 'name']; for (let i = 0; i < fallbackOrder.length; i++) { const key = fallbackOrder[i]; if (names[key]) return names[key]; } for (const key in names) { if (key.indexOf('name:') === 0 && names[key]) return names[key]; } return '未命名脚本'; } function compareVersions(a, b) { const pa = String(a || '').trim().replace(/^v/i, '').split(/[.-]/); const pb = String(b || '').trim().replace(/^v/i, '').split(/[.-]/); const len = Math.max(pa.length, pb.length); for (let i = 0; i < len; i++) { const na = parseInt(pa[i], 10); const nb = parseInt(pb[i], 10); if (!isNaN(na) && !isNaN(nb)) { if (na > nb) return 1; if (na < nb) return -1; } else { const sa = String(pa[i] || ''); const sb = String(pb[i] || ''); if (sa > sb) return 1; if (sa < sb) return -1; } } return 0; } function getInstalledRecord(id) { try { const raw = localStorage.getItem(INSTALLED_KEY_PREFIX + id); return raw ? JSON.parse(raw) : null; } catch (_) { return null; } } function setInstalledRecord(id, data) { try { localStorage.setItem(INSTALLED_KEY_PREFIX + id, JSON.stringify(data)); } catch (_) {} } function tryDisableInstalledScript(id) { try { if (!window.via) return; const via = window.via; if (typeof via.disableAddon === 'function') via.disableAddon(id); else if (typeof via.removeAddon === 'function') via.removeAddon(id); else if (typeof via.uninstallAddon === 'function') via.uninstallAddon(id); } catch (_) {} } const zhBase64 = { encode: function(input) { const _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; let output = "", chr1, chr2, chr3, enc1, enc2, enc3, enc4, i = 0; input = this.utf8_encode(input); while (i < input.length) { chr1 = input.charCodeAt(i++); chr2 = input.charCodeAt(i++); chr3 = input.charCodeAt(i++); enc1 = chr1 >> 2; enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); enc4 = chr3 & 63; if (isNaN(chr2)) enc3 = enc4 = 64; else if (isNaN(chr3)) enc4 = 64; output += _keyStr.charAt(enc1) + _keyStr.charAt(enc2) + _keyStr.charAt(enc3) + _keyStr.charAt(enc4); } return output; }, utf8_encode: function(_string) { return unescape(encodeURIComponent( Array.from(_string).map(char => { const codePoint = char.codePointAt(0); if (codePoint > 0x7F) { if (codePoint <= 0xFFFF) { return '\\u' + codePoint.toString(16).padStart(4, '0'); } else { return '\\u' + (0xD800 + ((codePoint - 0x10000) >> 10)).toString(16) + '\\u' + (0xDC00 + ((codePoint - 0x10000) & 0x3FF)).toString(16); } } return char; }).join('') )); } }; const extractPureScript = (text) => { if (!text) return null; const startRegex = /\/\/\s*==UserScript==/i; const startMatch = text.match(startRegex); if (!startMatch) return null; let lines = text.split('\n'); let lastValidLineIdx = -1; for (let i = lines.length - 1; i >= 0; i--) { if (lines[i].trim() !== "") { lastValidLineIdx = i; break; } } if (lastValidLineIdx === -1) return null; const lastLineContent = lines[lastValidLineIdx].trim(); const isEndValid = lastLineContent === ");" || lastLineContent.endsWith("})();") || lastLineContent === "})();"; if (isEndValid) { const cutText = lines.slice(0, lastValidLineIdx + 1).join('\n'); return cutText.substring(startMatch.index); } return null; }; function escapeRegExp(text) { return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function collectGrantApis(metaRaw) { const granted = []; const reg = /\/\/\s*@grant\s+([^\s]+)/gi; let match; while ((match = reg.exec(metaRaw)) !== null) { const api = match[1].trim(); if (!api || /^none$/i.test(api)) continue; if (granted.indexOf(api) === -1) granted.push(api); } return granted; } // 模拟支持的GM列表 const IMPLEMENTED_GM_APIS = { GM_info: 1, GM_getValue: 1, GM_setValue: 1, GM_deleteValue: 1, GM_listValues: 1, GM_addStyle: 1, GM_addElement: 1, GM_registerMenuCommand: 1, GM_unregisterMenuCommand: 1, GM_xmlhttpRequest: 1, GM_setClipboard: 1, GM_notification: 1, GM_updateNotification: 1, GM_openInTab: 1, GM_log: 1, GM_getResourceText: 1, GM_getResourceURL: 1, unsafeWindow: 1 }; function collectUsedApis(body) { const used = []; const reg = /\b(GM(?:_[A-Za-z0-9_]+)?|unsafeWindow)\b/g; let match; let steps = 0; while ((match = reg.exec(body)) !== null) { if (++steps > 20000) break; const api = match[1]; if (api === 'GM' || api === 'GM_POLYFILLED') continue; if (IMPLEMENTED_GM_APIS[api]) continue; if (used.indexOf(api) === -1) used.push(api); } return used; } function hasGmGuard(body, api) { const esc = escapeRegExp(api); const pattern = [ `typeof\\s+${esc}\\b`, `typeof\\s+window\\.${esc}\\b`, `['"]${esc}['"]\\s+in\\s+`, `window\\s*\\[\\s*['"]${esc}['"]\\s*\\]` ].join('|'); return new RegExp(pattern).test(body); } function analyzeGmCompat(metaRaw, content) { const granted = collectGrantApis(metaRaw); if (content.length > 200000) { const grantUnsupported = granted.filter(api => !IMPLEMENTED_GM_APIS[api]); return { granted: granted, guarded: [], unsupported: grantUnsupported, skipped: 'size' }; } const startedAt = Date.now(); const timeoutMs = 300; const body = content.replace(/\/\/\s*==UserScript==[\s\S]*?\/\/\s*==\/UserScript==/i, ''); const used = collectUsedApis(body); const guarded = []; const unsupported = []; for (let i = 0; i < used.length; i++) { if (Date.now() - startedAt > timeoutMs) return { granted: granted, guarded: guarded, unsupported: unsupported, skipped: 'timeout' }; const api = used[i]; if (hasGmGuard(body, api)) guarded.push(api); else unsupported.push(api); } return { granted: granted, guarded: guarded, unsupported: unsupported, skipped: null }; } function _alookGmPolyfill() { var W = window; if (W.GM_info && W.GM_info.scriptHandler && W.GM_info.scriptHandler !== 'ALook_Fake_GM') return; var POLYFILL_VERSION = 9; var UPGRADE = (W.__alookGMVersion || 0) < POLYFILL_VERSION; if (!UPGRADE) return; W.__alookGMVersion = POLYFILL_VERSION; W.__alookGMInstalled = true; var PF = W.GM_POLYFILLED = W.GM_POLYFILLED || {}; function reg(name, impl) { W[name] = impl; PF[name] = true; } var NS = '__alook_gm::' + location.hostname + '::'; function g(k, d) { try { var v = localStorage.getItem(NS + k); if (v === null) return d; if (v.length >= 2 && v.charAt(1) === '\u0000') { var t = v.charAt(0); var b = v.slice(2); if (t === 's') return b; if (t === 'n') return Number(b); if (t === 'b') return b === 'true'; if (t === 'x') return null; if (t === 'o') { try { var q = JSON.parse(b); return q === undefined ? d : q; } catch (e2) { return b; } } } try { var p = JSON.parse(v); if (p === undefined) return d; if (p === null || typeof p !== 'object') return v; return p; } catch (e) { return v; } } catch (e) { return d; } } function s(k, v) { try { var t = typeof v; var tag = (v === null) ? 'x' : (t === 'string' ? 's' : (t === 'number' ? 'n' : (t === 'boolean' ? 'b' : 'o'))); var body = (t === 'string') ? v : JSON.stringify(v); localStorage.setItem(NS + k, tag + '\u0000' + body); } catch (e) {} } function dl(k) { try { localStorage.removeItem(NS + k); } catch (e) {} } function ls() { var o = []; try { for (var i = 0; i < localStorage.length; i++) { var k = localStorage.key(i); if (k && k.indexOf(NS) === 0) o.push(k.slice(NS.length)); } } catch (e) {} return o; } function as(c) { try { var e = document.createElement('style'); e.textContent = c; (document.head || document.documentElement).appendChild(e); return e; } catch (e) { return null; } } function ae(a, b, c) { try { var p, t, x; if (typeof a === 'string') { t = a; x = b || {}; p = document.head || document.body; } else { p = a; t = b; x = c || {}; } var e = document.createElement(t); for (var k in x) { if (!Object.prototype.hasOwnProperty.call(x, k)) continue; var v = x[k]; if (k === 'textContent') e.textContent = v; else if (k === 'innerHTML') e.innerHTML = v; else if (k === 'style' && v && typeof v === 'object') { for (var sk in v) { if (Object.prototype.hasOwnProperty.call(v, sk)) { try { e.style[sk] = v[sk]; } catch (e2) {} } } } else if (k.indexOf('on') === 0 && k.length > 2 && typeof v === 'function') e.addEventListener(k.slice(2).toLowerCase(), v); else if (k === 'attributes' && v && typeof v === 'object') { for (var ak in v) { if (Object.prototype.hasOwnProperty.call(v, ak)) { try { e.setAttribute(ak, v[ak]); } catch (e2) {} } } } else { try { e.setAttribute(k, v); } catch (e2) {} } } p.appendChild(e); return e; } catch (e) { return null; } } var R = W.__alookGMMenu || (W.__alookGMMenu = { items: [], seq: 1, ui: null }); function sn() { try { var s = (W.GM_info && W.GM_info.script) || {}; return { id: s.id || ('name::' + (s.name || '未命名脚本')), name: s.name || '未命名脚本' }; } catch (e) { return { id: 'name::未命名脚本', name: '未命名脚本' }; } } function rm(n, f, a) { var id = R.seq++; var s = sn(); R.items.push({ id: id, name: n, fn: f, accessKey: a, scriptId: s.id, script: s.name }); bu(); rf(); return id; } function ru(id) { R.items = R.items.filter(function(c) { return c.id !== id; }); rf(); } function bu() { if (R.ui && document.body && document.body.contains(R.ui.root)) return; if (!document.body) { document.addEventListener('DOMContentLoaded', bu, { once: true }); return; } var r = document.createElement('div'); r.style.cssText = 'position:fixed;right:0;top:50%;transform:translateY(-50%);z-index:2147483647;user-select:none;touch-action:none;'; var sh = r.attachShadow({ mode: 'open' }); sh.innerHTML = '
GM
'; var b = sh.querySelector('.btn'), p = sh.querySelector('.panel'); var idleTimer, isDragging = false, startY, startTop, blockClickUntil = 0; var setIdle = function(on) { if (on) b.classList.add('idle'); else b.classList.remove('idle'); }; var resetIdle = function() { setIdle(false); clearTimeout(idleTimer); idleTimer = setTimeout(function() { if (!p.classList.contains('on')) setIdle(true); }, 3000); }; resetIdle(); b.addEventListener('click', function(e) { e.stopPropagation(); if (Date.now() < blockClickUntil) return; var opening = !p.classList.contains('on'); p.classList.toggle('on', opening); b.classList.toggle('on', opening); resetIdle(); if (opening) rf(); }); b.addEventListener('mouseenter', resetIdle); b.addEventListener('touchstart', function(e) { isDragging = false; startY = e.touches[0].clientY; startTop = r.offsetTop; b.style.transition = 'none'; resetIdle(); }, { passive: true }); b.addEventListener('touchmove', function(e) { var moveY = e.touches[0].clientY - startY; if (Math.abs(moveY) > 5) isDragging = true; if (isDragging) r.style.top = (startTop + moveY) + 'px'; }, { passive: true }); b.addEventListener('touchend', function() { b.style.transition = ''; if (isDragging) { isDragging = false; var ft = Math.max(60, Math.min(window.innerHeight - 60, r.offsetTop)); r.style.top = ft + 'px'; blockClickUntil = Date.now() + 300; } resetIdle(); }); document.addEventListener('click', function() { p.classList.remove('on'); b.classList.remove('on'); resetIdle(); }); document.body.appendChild(r); R.ui = { root: r, btn: b, panel: p, resetIdle: resetIdle }; } function rf() { if (!R.ui) return; var p = R.ui.panel; p.innerHTML = ''; if (R.items.length === 0) { p.innerHTML = '
无脚本菜单
'; return; } var ids = [], groups = {}; R.items.forEach(function(c) { var gid = c.scriptId || ('name::' + (c.script || '未命名脚本')); var gname = c.script || '未命名脚本'; if (!groups[gid]) { groups[gid] = { name: gname, items: [] }; ids.push(gid); } groups[gid].items.push(c); }); ids.forEach(function(gid) { var g = groups[gid]; var t = document.createElement('div'); t.className = 'group'; t.textContent = g.name; t.title = g.name; p.appendChild(t); g.items.forEach(function(c) { var d = document.createElement('div'); d.className = 'item'; d.textContent = typeof c.name === 'function' ? c.name() : c.name; d.addEventListener('click', function(e) { e.stopPropagation(); try { c.fn(); } catch (err) { console.error(err); } if (R.ui) { R.ui.panel.classList.remove('on'); R.ui.btn.classList.remove('on'); if (R.ui.resetIdle) R.ui.resetIdle(); } }); p.appendChild(d); }); }); } function xh(d) { var m = (d.method || 'GET').toUpperCase(), x = new XMLHttpRequest(), req = { readyState: 0, status: 0, statusText: '', responseText: '', response: '', responseHeaders: '', finalUrl: d.url, abort: function() { try { x.abort(); } catch (e) {} } }; var fir = function(n, a) { try { d[n] && d[n](a); } catch (e) {} }; try { if (d.user && d.password) x.open(m, d.url, true, d.user, d.password); else x.open(m, d.url, true); } catch (e) { setTimeout(function() { req.readyState = 4; fir('onreadystatechange', req); fir('onerror', req); }, 0); return req; } try { if (d.headers) { for (var hk in d.headers) { if (Object.prototype.hasOwnProperty.call(d.headers, hk)) { try { x.setRequestHeader(hk, d.headers[hk]); } catch (e) {} } } if (d.cookie) { try { x.setRequestHeader('Cookie', d.cookie); } catch (e) {} } } } catch (e) {} try { if (d.responseType) x.responseType = d.responseType; } catch (e) {} try { if (d.timeout) x.timeout = d.timeout; } catch (e) {} try { if (d.overrideMimeType) x.overrideMimeType(d.overrideMimeType); } catch (e) {} try { if (d.anonymous) x.withCredentials = false; else if (d.credentials === true || d.withCredentials === true) x.withCredentials = true; } catch (e) {} var rsp = function() { try { req.readyState = x.readyState; if (x.readyState >= 2) { req.status = x.status; req.statusText = x.statusText; try { req.responseHeaders = x.getAllResponseHeaders() || ''; } catch (e) {} } if (x.readyState === 4) { var rt = d.responseType || ''; try { if (rt === 'json') { req.response = x.response; try { req.responseText = JSON.stringify(x.response); } catch (e) { req.responseText = ''; } } else if (rt === 'arraybuffer' || rt === 'blob' || rt === 'document') { req.response = x.response; req.responseText = ''; } else { req.responseText = x.responseText; req.response = x.responseText; } } catch (e) {} } } catch (e) {} }; x.onreadystatechange = function() { rsp(); fir('onreadystatechange', req); }; x.onloadstart = function() { fir('onloadstart', req); }; x.onprogress = function(e) { try { req.lengthComputable = e.lengthComputable; req.loaded = e.loaded; req.total = e.total; } catch (e2) {} fir('onprogress', req); }; x.onload = function() { rsp(); fir('onload', req); }; x.onerror = function() { rsp(); fir('onerror', req); }; x.ontimeout = function() { rsp(); fir('ontimeout', req); }; x.onabort = function() { rsp(); fir('onabort', req); }; try { if (m === 'GET' || m === 'HEAD' || d.data == null) x.send(); else x.send(d.data); } catch (e) { setTimeout(function() { req.readyState = 4; fir('onreadystatechange', req); fir('onerror', req); }, 0); } return req; } var NT = W.__alookGMNotify || (W.__alookGMNotify = { list: [], seq: 1, root: null, sh: null }); function ntr() { if (NT.sh && NT.root && NT.root.parentNode && document.contains(NT.root)) return; if (!document.body) { document.addEventListener('DOMContentLoaded', ntr, { once: true }); return; } var r = document.createElement('div'); r.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:2147483647;display:flex;flex-direction:column-reverse;gap:8px;pointer-events:none;'; var sh = r.attachShadow({ mode: 'open' }); sh.innerHTML = ''; document.body.appendChild(r); NT.root = r; NT.sh = sh; } function ndr(o) { var w = o.el; if (o.title != null) { var t = w.querySelector('.t'); if (!t) { t = document.createElement('div'); t.className = 't'; w.insertBefore(t, w.firstChild); } t.textContent = o.title; } if (o.text != null) { var x = w.querySelector('.x'); if (!x) { x = document.createElement('div'); x.className = 'x'; var pp = w.querySelector('.p'); if (pp) w.insertBefore(x, pp); else w.appendChild(x); } x.textContent = o.text; } if (o.progress != null) { var p = w.querySelector('.p'); if (!p) { p = document.createElement('div'); p.className = 'p'; p.innerHTML = ''; w.appendChild(p); } p.firstChild.style.width = Math.max(0, Math.min(100, o.progress)) + '%'; } } function nfin(o, byUser) { if (o.done) return; o.done = true; clearTimeout(o.tm); var w = o.el; w.classList.remove('on'); setTimeout(function () { try { w.remove(); } catch (e) {} NT.list = NT.list.filter(function (x) { return x !== o; }); }, 300); try { o.ondone && o.ondone(byUser); } catch (e) {} } function sc(t) { var v = String(t); var ok = false; try { var a = document.createElement('textarea'); a.value = v; a.setAttribute('readonly', ''); a.style.cssText = 'position:fixed;left:-9999px;top:-9999px;opacity:0'; (document.body || document.documentElement).appendChild(a); a.focus(); a.select(); try { a.setSelectionRange(0, v.length); } catch (e) {} try { ok = document.execCommand('copy'); } catch (e) { ok = false; } try { a.parentNode && a.parentNode.removeChild(a); } catch (e) {} } catch (e) { ok = false; } if (ok) return true; try { if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { navigator.clipboard.writeText(v).catch(function () {}); return true; } } catch (e) {} return false; } function nt(a, b, c, e) { var o = typeof a === 'string' ? { text: a, title: b, image: c, onclick: e } : (a || {}); ntr(); if (!NT.sh) { try { console.log('[GM_notification]', o.title || '', o.text || ''); } catch (_) {} return { id: -1, remove: function () {} }; } o.id = NT.seq++; o.done = false; var w = o.el = document.createElement('div'); w.className = 'n'; NT.sh.appendChild(w); NT.list.push(o); ndr(o); requestAnimationFrame(function () { w.classList.add('on'); }); if (o.timeout) o.tm = setTimeout(function () { nfin(o, false); }, o.timeout); w.addEventListener('click', function (ev) { try { o.onclick && o.onclick(ev); } catch (e) {} nfin(o, true); }); if (o.oncreate) try { o.oncreate(o.id); } catch (e) {} return { id: o.id, remove: function () { nfin(o, false); } }; } function nu(id, o) { for (var i = 0; i < NT.list.length; i++) { var it = NT.list[i]; if (it.id === id) { if (o.title != null) it.title = o.title; if (o.text != null) it.text = o.text; if (o.progress != null) it.progress = o.progress; ndr(it); return; } } } function oit(u) { var w = window.open(u, '_blank'); return { close: function() { try { w && w.close(); } catch (e) {} }, get closed() { try { return !w || w.closed; } catch (e) { return true; } } }; } reg('unsafeWindow', W); reg('GM_getValue', g); reg('GM_setValue', s); reg('GM_deleteValue', dl); reg('GM_listValues', ls); reg('GM_addStyle', as); reg('GM_addElement', ae); reg('GM_registerMenuCommand', rm); reg('GM_unregisterMenuCommand', ru); reg('GM_xmlhttpRequest', xh); reg('GM_setClipboard', sc); reg('GM_notification', nt); reg('GM_updateNotification', nu); reg('GM_openInTab', oit); reg('GM_log', function() { try { console.log.apply(console, arguments); } catch (e) {} }); reg('GM_getResourceText', function(n) { console.warn('[GM] @resource 未支持:', n); return ''; }); reg('GM_getResourceURL', W.GM_getResourceText); reg('GM', { getValue: function(k, d) { return Promise.resolve(g(k, d)); }, setValue: function(k, v) { s(k, v); return Promise.resolve(); }, deleteValue: function(k) { dl(k); return Promise.resolve(); }, listValues: function() { return Promise.resolve(ls()); }, addStyle: function(c) { return Promise.resolve(as(c)); }, addElement: function(a, b, c) { return Promise.resolve(ae(a, b, c)); }, registerMenuCommand: rm, unregisterMenuCommand: ru, xmlHttpRequest: function(d) { return new Promise(function(res, rej) { var ol = d.onload, oe = d.onerror, ot = d.ontimeout; xh(Object.assign({}, d, { onload: function(r) { res(r); ol && ol(r); }, onerror: function(r) { rej(r); oe && oe(r); }, ontimeout: function(r) { rej(r); ot && ot(r); } })); }); }, setClipboard: function(t) { sc(t); return Promise.resolve(); }, notification: nt, updateNotification: nu, openInTab: oit, log: function() { try { console.log.apply(console, arguments); } catch (e) {} } }); W.GM_info = W.GM_info || {}; W.GM_info.scriptHandler = 'ALook_Fake_GM'; W.GM_info.version = '1.0'; W.GM_info.versionCode = POLYFILL_VERSION; W.GM_info.script = W.GM_info.script || { name: '', namespace: '', version: '', matches: [] }; } const POLYFILL = '(' + _alookGmPolyfill.toString() + ')();'; const installFromContent = async (rawContent, sourceLabel) => { if (!window.via?.addon) { alert('当前环境不支持安装脚本(window.via.addon 不存在)。'); return; } try { const content = extractPureScript(rawContent) || rawContent; if (!content) { alert('未能从以下来源提取用户脚本内容:\n' + (sourceLabel || '未知来源')); return; } const metaMatch = content.match(/\/\/\s*==UserScript==([\s\S]*?)\/\/\s*==\/UserScript==/i); const metaRaw = metaMatch ? metaMatch[1] : ''; const sensitiveKeywords = ['resource', 'require', 'connect']; let foundKeywords = []; sensitiveKeywords.forEach(kw => { const reg = new RegExp(`\\/\\/\\s*@${kw}\\s+`, 'i'); if (reg.test(metaRaw)) foundKeywords.push(`@${kw}`); }); if (foundKeywords.length > 0) { if (!confirm(`该脚本包含 ALook 可能不支持的指令:\n[ ${foundKeywords.join(', ')} ]\n\n建议检查兼容性。是否继续安装?`)) return; } const gmCompat = analyzeGmCompat(metaRaw, content); if (gmCompat.skipped === 'size') { const grantList = gmCompat.unsupported.length > 0 ? `\n\n@grant 声明中 ALook 未实现的 API:\n[ ${gmCompat.unsupported.join(', ')} ]` : '\n\n@grant 声明的 API 均已在 polyfill 中实现。'; if (!confirm(`脚本正文超过 200000 字符,已跳过深度扫描,仅比对 @grant 声明。${grantList}\n\n是否继续安装?`)) return; } else if (gmCompat.skipped === 'timeout') { const extra = gmCompat.unsupported.length > 0 ? `\n\n已扫描到的未实现 API:\n[ ${gmCompat.unsupported.join(', ')} ]` : '\n\n暂未扫描到未实现 API(检测未完成,不保证完整)。'; if (!confirm(`GM 兼容性检测超时(300ms),未能完成全部 API 扫描。${extra}\n\n是否继续安装?`)) return; } else if (gmCompat.unsupported.length > 0) { if (!confirm(`该脚本直接依赖 ALook 未实现的 GM API:\n[ ${gmCompat.unsupported.join(', ')} ]\n\n脚本未做降级处理,可能无法正常运行。是否继续安装?`)) return; } const meta = {}; metaRaw.split('\n').forEach(line => { const match = line.match(/\/\/\s*@([^\s]+)\s+(.*)/); if (match) { const key = match[1].toLowerCase(); meta[key] = (meta[key] || []).concat(match[2].trim()); } }); const runAtMatch = metaRaw.match(/\/\/\s*@run-at\s+(.+)/i); const runatValue = (!runAtMatch || runAtMatch[1].trim() === 'document-start') ? 1 : 0; const displayName = pickScriptName(meta); const stableName = (meta.name || ['未命名脚本'])[0]; const scriptNamespace = (meta.namespace || [''])[0]; const scriptVersion = (meta.version || ['1.0'])[0]; const scriptMatches = meta.match || meta.include || []; const scriptUrlKey = scriptMatches.join('@@'); const scriptId = makeScriptId(stableName, scriptNamespace, scriptUrlKey); const installed = getInstalledRecord(scriptId); if (installed && installed.version) { const cmp = compareVersions(installed.version, scriptVersion); if (cmp === 0) { if (!confirm(`脚本已安装相同版本:${displayName} ${scriptVersion}\n\n继续安装,建议先禁用已安装版本,再以当前内容重新安装。是否继续?`)) return; } else if (cmp > 0) { if (!confirm(`已安装 ${displayName} ${installed.version},此次为降级安装(版本号 ${installed.version} -> ${scriptVersion})。是否继续?`)) return; } else { if (!confirm(`已安装 ${displayName} ${installed.version},此次为升级安装(版本号 ${installed.version} -> ${scriptVersion})。是否继续?`)) return; } tryDisableInstalledScript(scriptId); } const metaJson = JSON.stringify({ id: scriptId, name: displayName, namespace: scriptNamespace, version: scriptVersion, matches: scriptMatches }); const config = { id: scriptId, name: displayName, author: (meta.author || ['未知作者'])[0], version: scriptVersion, runat: runatValue, url: (meta.match || meta.include || ['*']).map(rule => rule.replace(/^https?:\/\//, 'http*://*').replace(/\*/g, '.*') ).join('@@'), code: btoa(unescape(encodeURIComponent(`(function(){\n\n${POLYFILL}\n\nif(window.GM_info)window.GM_info.script=${metaJson};\n\nvar GM_registerMenuCommand=(typeof window.GM_registerMenuCommand==='function')?function(n,f,a){var p=window.GM_info.script;window.GM_info.script=${metaJson};try{return window.GM_registerMenuCommand(n,f,a);}finally{window.GM_info.script=p;}}:undefined;\n\n${content}\n\n})();`))) }; window.via.addon(zhBase64.encode(JSON.stringify(config))); setInstalledRecord(scriptId, { version: scriptVersion, name: displayName, namespace: scriptNamespace, url: scriptUrlKey, time: Date.now() }); } catch (e) { console.error(e); } }; const installScript = async () => { if (!window.via?.addon) return; try { let content = extractPureScript(document.body.innerText); if (!content) { const res = await fetch(location.href, { cache: 'no-cache' }); content = await res.text(); } if (!content) return; await installFromContent(content, location.href); } catch (e) { console.error(e); } }; const fetchScriptByUrl = (url, callback) => { const done = (text) => { if (!text) { alert('获取脚本内容失败:内容为空。'); return; } callback(text); }; const openInTab = (reason) => { if (confirm('无法直接获取脚本内容' + (reason ? '(' + reason + ')' : '') + '。\n\n是否在新标签打开该链接,由直装助手自动接管安装?\n' + url)) { window.open(url, '_blank'); } }; const fallback = () => { fetch(url, { cache: 'no-cache' }).then((r) => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); }).then(done).catch((e) => { console.error(e); openInTab(e && e.message ? e.message : String(e)); }); }; if (typeof window.GM_xmlhttpRequest === 'function') { try { window.GM_xmlhttpRequest({ method: 'GET', url: url, timeout: 20000, onload: (r) => { if (r.status >= 200 && r.status < 400 && r.responseText) done(r.responseText); else fallback(); }, onerror: () => { fallback(); }, ontimeout: () => { fallback(); }}); return; } catch (e) { console.error(e); } } fallback(); }; try { _alookGmPolyfill(); } catch (e) { console.error(e); } const SELF_META = { name: 'ALook浏览器脚本直装助手(GM)', namespace: 'https://www.alookweb.com/', version: '1.50', matches: ['*://*/*'] }; if (typeof window.GM_registerMenuCommand === 'function') { const registerSelfMenuCommand = (name, fn) => { const prev = window.GM_info ? window.GM_info.script : undefined; if (window.GM_info) window.GM_info.script = SELF_META; try { window.GM_registerMenuCommand(name, fn); } finally { if (window.GM_info) window.GM_info.script = prev; } }; registerSelfMenuCommand('直链安装脚本', () => { const input = prompt('请输入用户脚本的直链 URL:', ''); if (!input) return; const url = input.trim(); if (!/^https?:\/\//i.test(url)) { alert('URL 必须以 http:// 或 https:// 开头。'); return; } fetchScriptByUrl(url, (text) => installFromContent(text, url)); }); registerSelfMenuCommand('本地文件安装脚本', () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.js,application/javascript,text/javascript'; input.style.cssText = 'position:fixed;left:-9999px;opacity:0'; (document.body || document.documentElement).appendChild(input); input.addEventListener('change', () => { const file = input.files && input.files[0]; try { input.remove(); } catch (e) {} if (!file) return; const reader = new FileReader(); reader.onload = () => installFromContent(String(reader.result || ''), file.name); reader.onerror = () => alert('读取文件失败:' + (file.name || '')); reader.readAsText(file, 'utf-8'); }); input.click(); }); } if (/\.(user|userscript)\.js(\?|$)/i.test(location.href)) { if (document.readyState === 'complete') installScript(); else window.addEventListener('load', installScript, { once: true }); } })();