// ==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
});
}
})();