// ==UserScript== // @name ALook 浏览器 脚本直装助手 // @namespace http://tampermonkey.net/ // @version 1.4.7 // @description 还原ALook原生安装协议识别并安装user.js后缀的脚本。 // @author Grok && Gemini // @match http*://*/*.user.js // @match http*://*/*.userscript.js // @grant none // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; const SCRIPT_ID_SALT = 'alook-userscript: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(name, namespace) { const ns = String(namespace || '').trim(); const key = SCRIPT_ID_SALT + normalizeScriptName(name) + '\n' + ns; return `userscript-${fnv1a64(key)}`; } 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; } function collectUsedApis(body) { const used = []; const reg = /\b(GM(?:_[A-Za-z0-9_]+)?|unsafeWindow)\b/g; let match; while ((match = reg.exec(body)) !== null) { const api = match[1]; if (api === 'GM') 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 body = content.replace(/\/\/\s*==UserScript==[\s\S]*?\/\/\s*==\/UserScript==/i, ''); const granted = collectGrantApis(metaRaw); const used = collectUsedApis(body); const guarded = []; const unsupported = []; used.forEach(api => { if (hasGmGuard(body, api)) guarded.push(api); else unsupported.push(api); }); return { granted: granted, guarded: guarded, unsupported: unsupported }; } 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' }); const remoteText = await res.text(); content = extractPureScript(remoteText) || remoteText; } 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.unsupported.length > 0) { if (!confirm(`该脚本直接依赖 ALook 不支持的 GM 环境:\n[ ${gmCompat.unsupported.join(', ')} ]\n\nALook 未实现 GM 函数且脚本未做降级处理,可能无法正常运行。是否继续安装?`)) return; } const meta = {}; metaRaw.split('\n').forEach(line => { const match = line.match(/\/\/\s*@(\w+)\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 scriptName = (meta.name || ['未命名脚本'])[0]; const scriptNamespace = (meta.namespace || [''])[0]; const config = { id: makeScriptId(scriptName, scriptNamespace), name: scriptName, author: (meta.author || ['未知作者'])[0], version: (meta.version || ['1.0'])[0], runat: runatValue, url: (meta.match || meta.include || ['*']).map(rule => rule.replace(/^https?:\/\//, 'http*://*').replace(/\*/g, '.*') ).join('@@'), code: btoa(unescape(encodeURIComponent(`(function(){\n\n${content}\n\n})();`))) }; window.via.addon(zhBase64.encode(JSON.stringify(config))); } catch (e) { console.error(e); } }; if (/\.(user|userscript)\.js(\?|$)/i.test(location.href)) { if (document.readyState === 'complete') installScript(); else window.addEventListener('load', installScript, { once: true }); } })();