// ==UserScript== // @name AIX 媒体嗅探下载器(油猴版) // @namespace aix.userjs.port // @version 1.0.0 // @description 移植自 AIX智能下载器 9.0.58:自动嗅探网页中的视频 / 音频 / 图片资源,悬浮面板预览、一键下载、复制链接;内置 B站 / YouTube / TikTok / Instagram / 淘宝 / 百度百科专属提取。 // @author AIX UserJS Port // @match *://*/* // @exclude *://chrome.google.com/* // @exclude *://microsoftedge.microsoft.com/* // @grant GM_download // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_setClipboard // @grant GM_registerMenuCommand // @grant unsafeWindow // @connect * // @run-at document-start // @license MIT // ==/UserScript== (function () { 'use strict'; /* ================================ 配置 ================================ */ const CFG = { maxItems: 400, // 列表最大条数 minImgBytes: 20480, // 图片嗅探最小体积(字节,仅对能拿到大小的来源生效) scanInterval: 3000, // DOM 扫描周期 ballSize: 48, }; const MEDIA = { video: /\.(mp4|webm|flv|mkv|mov|m4v|ogv|avi|wmv|3gp)(\?|#|$)/i, hls: /\.(m3u8)(\?|#|$)/i, audio: /\.(mp3|m4a|aac|flac|wav|ogg|opus|wma)(\?|#|$)/i, image: /\.(jpe?g|png|gif|webp|bmp|avif|apng)(\?|#|$)/i, }; const MIME_RE = /^(video|audio|image)\//i; const IS_TOP = (() => { try { return window.self === window.top; } catch (e) { return false; } })(); /* ================================ 工具 ================================ */ const uid = () => Math.random().toString(36).slice(2, 10) + Date.now().toString(36); function extFromUrl(url) { try { const p = new URL(url, location.href).pathname; const m = p.match(/\.([a-z0-9]{2,5})$/i); return m ? m[1].toLowerCase() : ''; } catch (e) { return ''; } } function classify(url, mime) { if (mime && MIME_RE.test(mime)) { const t = mime.split('/')[0].toLowerCase(); return t === 'image' ? 'image' : t === 'audio' ? 'audio' : 'video'; } if (MEDIA.hls.test(url)) return 'hls'; if (MEDIA.video.test(url)) return 'video'; if (MEDIA.audio.test(url)) return 'audio'; if (MEDIA.image.test(url)) return 'image'; return ''; } function sanitizeName(s) { return String(s || '').replace(/[\\/:*?"<>|\r\n\t]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 120); } function fileNameFromUrl(url) { try { const p = decodeURIComponent(new URL(url, location.href).pathname); return sanitizeName(p.split('/').filter(Boolean).pop() || 'media'); } catch (e) { return 'media'; } } function hostOf(url) { try { return new URL(url, location.href).hostname; } catch (e) { return ''; } } function fmtSize(n) { if (n == null || isNaN(n)) return ''; if (n < 1024) return n + ' B'; if (n < 1048576) return (n / 1024).toFixed(1) + ' KB'; if (n < 1073741824) return (n / 1048576).toFixed(1) + ' MB'; return (n / 1073741824).toFixed(2) + ' GB'; } function gmXhr(opt) { return new Promise((resolve, reject) => { GM_xmlhttpRequest(Object.assign({}, opt, { onload: (r) => resolve(r), onerror: (e) => reject(e), ontimeout: () => reject(new Error('timeout')), })); }); } /* ================================ 数据仓库 ================================ */ const store = { map: new Map(), order: [], add(item) { if (!item || !item.url) return false; if (/^(data|blob):/i.test(item.url)) return false; if (store.map.has(item.url)) { const old = store.map.get(item.url); if (item.size != null && old.size == null) { old.size = item.size; if (panel.visible) panel.refreshRow(old); } return false; } const it = Object.assign({ id: uid(), type: 'video', title: '', ext: '', size: null, pageOrigin: location.origin, ts: Date.now(), quality: '', }, item); if (!it.ext) it.ext = extFromUrl(it.url) || (it.type === 'hls' ? 'm3u8' : ''); store.map.set(it.url, it); store.order.push(it); if (store.order.length > CFG.maxItems) { const rm = store.order.shift(); store.map.delete(rm.url); } if (IS_TOP) ui.onAdded(it); return true; }, list(type) { let arr = store.order; if (type && type !== 'all') arr = arr.filter(i => i.type === type || (type === 'video' && i.type === 'hls')); return arr.slice().reverse(); }, clear() { store.map.clear(); store.order = []; if (IS_TOP) ui.render(); }, }; /* ================================ 嗅探引擎 ================================ */ const sniffer = { started: false, start() { if (sniffer.started) return; sniffer.started = true; sniffer.hookFetch(); sniffer.hookXHR(); sniffer.hookPerformance(); }, captureUrl(url, mime, size) { if (!url) return; if (/^(data|blob):/i.test(url)) return; const type = classify(url, mime); if (!type) return; if (type === 'image' && size != null && size > 0 && size < CFG.minImgBytes) return; store.add({ url, type, title: fileNameFromUrl(url), size: size || null, }); }, hookFetch() { try { const orig = window.fetch; if (!orig || orig.__aix) return; const wrapped = function (input, init) { let url = ''; try { url = typeof input === 'string' ? input : (input && input.url) || ''; } catch (e) {} return orig.apply(this, arguments).then(res => { try { const ct = (res.headers && res.headers.get('content-type')) || ''; sniffer.captureUrl(res.url || url, ct); } catch (e) {} return res; }); }; wrapped.__aix = true; window.fetch = wrapped; } catch (e) {} }, hookXHR() { try { const origOpen = XMLHttpRequest.prototype.open; const origSend = XMLHttpRequest.prototype.send; if (origOpen.__aix) return; XMLHttpRequest.prototype.open = function (method, url) { this.__aixUrl = url; return origOpen.apply(this, arguments); }; XMLHttpRequest.prototype.open.__aix = true; XMLHttpRequest.prototype.send = function () { this.addEventListener('load', function () { try { const ct = this.getResponseHeader('content-type') || ''; if (MIME_RE.test(ct) || classify(String(this.__aixUrl || this.responseURL || ''))) { sniffer.captureUrl(this.responseURL || this.__aixUrl, ct); } } catch (e) {} }); return origSend.apply(this, arguments); }; } catch (e) {} }, hookPerformance() { try { const po = new PerformanceObserver((list) => { for (const e of list.getEntries()) { const t = classify(e.name); if (!t) continue; if (t === 'image' && e.transferSize && e.transferSize < CFG.minImgBytes) continue; sniffer.captureUrl(e.name, '', e.transferSize || null); } }); po.observe({ entryTypes: ['resource'] }); } catch (e) {} }, /* DOM 扫描:video / audio / source / img / og:meta */ domScan() { try { document.querySelectorAll('video, audio, source').forEach(el => { const u = el.currentSrc || el.src || el.getAttribute('src') || ''; if (u && !/^(data|blob):/i.test(u)) { store.add({ url: u, type: classify(u) || (el.tagName === 'AUDIO' ? 'audio' : 'video'), title: document.title }); } const poster = el.getAttribute && el.getAttribute('poster'); if (poster) store.add({ url: poster, type: 'image', title: fileNameFromUrl(poster) }); }); document.querySelectorAll('img').forEach(el => { const u = el.currentSrc || el.src || ''; if (u && !/^(data|blob):/i.test(u) && (el.naturalWidth || 0) >= 200) { store.add({ url: u, type: 'image', title: fileNameFromUrl(u), size: null }); } }); const og = document.querySelector('meta[property="og:video"], meta[property="og:video:url"], meta[property="og:audio"]'); if (og && og.content) store.add({ url: og.content, type: classify(og.content) || 'video', title: document.title }); } catch (e) {} }, startDomScan() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setInterval(() => sniffer.domScan(), CFG.scanInterval), { once: true }); } else { setInterval(() => sniffer.domScan(), CFG.scanInterval); } window.addEventListener('load', () => setTimeout(() => sniffer.domScan(), 500)); }, }; /* iframe 嗅探结果转发到顶层窗口 */ function forwardToTop() { if (IS_TOP) { window.addEventListener('message', (ev) => { const d = ev.data; if (d && d.__aixMedia && Array.isArray(d.items)) d.items.forEach(it => store.add(it)); }); } else { const origAdd = store.add.bind(store); store.add = function (item) { const ok = origAdd(item); try { window.top.postMessage({ __aixMedia: true, items: [item] }, '*'); } catch (e) {} return ok; }; } } /* ================================ 站点专属提取 ================================ */ const handlers = { /* ---------- 哔哩哔哩:通过 html5 playurl 接口取 mp4 直链 ---------- */ bilibili() { if (!/^(www|m)\.bilibili\.com$/.test(location.hostname)) return; const match = location.pathname.match(/\/video\/(BV\w+)/) || location.pathname.match(/\/video\/av(\d+)/i); if (!match) return; let st = null; try { st = unsafeWindow.__INITIAL_STATE__; } catch (e) {} const vd = st && st.videoData; if (!vd || !vd.aid || !vd.cid) return; let cid = vd.cid + ''; const q = new URLSearchParams(location.search); if (q.get('p') && Array.isArray(vd.pages)) { const pg = vd.pages[parseInt(q.get('p'), 10) - 1]; if (pg) cid = pg.cid + ''; } const api = `https://api.bilibili.com/x/player/playurl?aid=${vd.aid}&cid=${cid}&bvid=${vd.bvid || ''}&qn=80&platform=html5`; gmXhr({ method: 'GET', url: api, headers: { Referer: 'https://www.bilibili.com' } }) .then(r => { const j = JSON.parse(r.responseText); if (!j.data || !j.data.durl || !j.data.durl.length) return; const d = j.data.durl[0]; const title = sanitizeName(vd.title + (vd.pages && vd.pages.length > 1 ? '_P' + (parseInt(q.get('p') || '1', 10)) : '')); store.add({ url: d.url, type: 'video', title: title || 'bilibili_video', size: d.size, quality: j.data.quality + 'P', ext: 'mp4', }); if (j.data.durl.length > 1) { j.data.durl.slice(1).forEach((seg, i) => store.add({ url: seg.url, type: 'video', title: title + '_part' + (i + 2), size: seg.size, quality: '分段', ext: 'mp4', })); } }).catch(() => {}); }, /* ---------- YouTube:解析 ytInitialPlayerResponse ---------- */ youtube() { if (location.hostname !== 'www.youtube.com' || !/\/watch/.test(location.pathname + location.search)) return; let raw = null; try { raw = unsafeWindow.ytInitialPlayerResponse; } catch (e) {} if (!raw) { for (const s of document.querySelectorAll('script')) { const txt = s.textContent || ''; const i = txt.indexOf('ytInitialPlayerResponse'); if (i < 0) continue; const brace = txt.indexOf('{', i); if (brace < 0) continue; const json = extractJSONObject(txt, brace); if (json) { try { raw = JSON.parse(json); } catch (e) {} } if (raw) break; } } if (!raw || !raw.streamingData) return; const title = sanitizeName((raw.videoDetails && raw.videoDetails.title) || document.title.replace(' - YouTube', '')); const prog = raw.streamingData.formats || []; const adap = raw.streamingData.adaptiveFormats || []; prog.forEach(f => { if (!f.url) return; store.add({ url: f.url, type: 'video', title: title + '_' + (f.qualityLabel || f.itag), size: Number(f.contentLength) || null, quality: (f.qualityLabel || '') + '(音视频)', ext: f.mimeType && /mp4/.test(f.mimeType) ? 'mp4' : 'webm', }); }); adap.forEach(f => { if (!f.url) return; const mt = f.mimeType || ''; if (/^video\//.test(mt) && /mp4/.test(mt)) { store.add({ url: f.url, type: 'video', title: title + '_' + (f.qualityLabel || '') + '(仅画面)', size: Number(f.contentLength) || null, quality: f.qualityLabel + ' 纯视频', ext: 'mp4', }); } else if (/^audio\/mp4/.test(mt)) { store.add({ url: f.url, type: 'audio', title: title + '_音频' + (Math.round((f.averageBitrate || 0) / 1000) + 'kbps' || ''), size: Number(f.contentLength) || null, quality: Math.round((f.averageBitrate || 0) / 1000) + ' kbps', ext: 'm4a', }); } }); }, /* ---------- TikTok:仿客户端接口取无水印直链 ---------- */ tiktok() { if (!/(^|\.)tiktok\.com$/.test(location.hostname)) return; document.querySelectorAll('div[mode]').forEach(wrap => { const o = wrap.querySelector('div[id^=xgwrapper-]'); if (!o) return; const id = o.id.split('-').pop(); if (!id || handlers._ttDone && handlers._ttDone.has(id)) return; handlers._ttDone = handlers._ttDone || new Set(); handlers._ttDone.add(id); const rnd = (chars, n) => { let s = ''; for (let i = 0; i < n; i++) s += chars[Math.floor(Math.random() * chars.length)]; return s; }; const ts = Math.round(Date.now() / 1000); const p = { aweme_id: id, version_name: '26.1.3', version_code: '260103', build_number: '26.1.3', manifest_version_code: '260103', update_version_code: '260103', openudid: rnd('0123456789abcdef', 16), uuid: rnd('0123456789', 16), _rticket: 1000 * ts, ts, device_brand: 'Google', device_type: 'Pixel 4', device_platform: 'android', resolution: '1080*1920', dpi: 420, os_version: '10', os_api: '29', carrier_region: 'US', sys_region: 'US', region: 'US', app_name: 'trill', app_language: 'en', language: 'en', timezone_name: 'America/New_York', timezone_offset: '-14400', channel: 'googleplay', ac: 'wifi', mcc_mnc: '310260', is_my_cn: 0, aid: 1180, ssmix: 'a', as: 'a1qwert123', cp: 'cbfhckdckkde1', }; const url = 'https://api-h2.tiktokv.com/aweme/v1/feed/?' + Object.keys(p).map(k => k + '=' + p[k]).join('&'); gmXhr({ method: 'GET', url }) .then(r => { const j = JSON.parse(r.responseText); const a = j.aweme_list && j.aweme_list[0]; if (!a) return; const u = a.video && a.video.play_addr && a.video.play_addr.url_list[0]; if (!u) return; const t = sanitizeName((a.desc || document.title).slice(0, 60)); store.add({ url: u, type: 'video', title: t, quality: '无水印', ext: 'mp4' }); }).catch(() => {}); }); }, /* ---------- Instagram:解析页面内嵌 JSON 中的 video_versions ---------- */ instagram() { if (!/(^|\.)instagram\.com$/.test(location.hostname)) return; const re = /"video_versions":\[\{[^{}]*?"url":"(https:[^"]+)"/g; document.querySelectorAll('script').forEach(s => { const txt = s.textContent || ''; if (txt.indexOf('video_versions') < 0) return; let m, count = 0; while ((m = re.exec(txt)) && count < 10) { const u = m[1].replace(/\\u0026/g, '&').replace(/\\\//g, '/'); store.add({ url: u, type: 'video', title: sanitizeName(document.title), quality: 'IG', ext: 'mp4' }); count++; } }); }, /* ---------- 淘宝商品视频 ---------- */ taobao() { if (!/(^|\.)taobao\.com$/.test(location.hostname)) return; const meta = document.querySelector('meta[name="microscope-data"]'); if (!meta) return; const uidM = (meta.content || '').match(/userid=(\d+);/); if (!uidM) return; for (const s of document.querySelectorAll('script')) { const m = (s.innerText || '').match(/"videoId":"(\d+)"/); if (m) { const url = `https://cloud.video.taobao.com/play/u/${uidM[1]}/p/1/e/6/t/1/${m[1]}.mp4`; store.add({ url, type: 'video', title: m[1], ext: 'mp4' }); break; } } }, /* ---------- 百度百科 词条视频 ---------- */ baike() { if (location.hostname !== 'baike.baidu.com' || !/\/item\//.test(location.pathname)) return; let lemmaId = null; try { lemmaId = unsafeWindow.lemmaId; } catch (e) {} if (!lemmaId) { const m = (document.documentElement.innerHTML || '').match(/lemmaId["':=\s]+(\d+)/); if (m) lemmaId = m[1]; } if (!lemmaId) return; let pn = 1; const loop = () => { gmXhr({ method: 'GET', url: `https://baike.baidu.com/api/wikisecond/lemmavideo?lemmaId=${lemmaId}&pn=${pn}&isSensitive=0&dataSource=related` }) .then(r => { const j = JSON.parse(r.responseText); if (j.list && j.list.length) { j.list.forEach(v => { if (v.playMp4Url) store.add({ url: v.playMp4Url, type: 'video', title: sanitizeName(v.title || document.title), ext: 'mp4' }); }); pn++; loop(); } }).catch(() => {}); }; loop(); }, run() { [this.bilibili, this.youtube, this.tiktok, this.instagram, this.taobao, this.baike].forEach(h => { try { h(); } catch (e) {} }); }, }; /* 从文本中提取一个完整的 JSON 对象(括号配平,忽略字符串内的括号) */ function extractJSONObject(text, startIdx) { let depth = 0, inStr = false, esc = false; for (let i = startIdx; i < text.length && i < startIdx + 2000000; i++) { const c = text[i]; if (esc) { esc = false; continue; } if (c === '\\') { esc = true; continue; } if (c === '"') { inStr = !inStr; continue; } if (inStr) continue; if (c === '{') depth++; else if (c === '}') { depth--; if (depth === 0) return text.slice(startIdx, i + 1); } } return null; } /* ================================ 下载 ================================ */ const downloader = { headCache: new Map(), fetchSize(item) { if (item.size != null || item._sizeAsked) return; item._sizeAsked = true; gmXhr({ method: 'HEAD', url: item.url, headers: { Referer: item.pageOrigin || location.origin }, timeout: 8000 }) .then(r => { const cl = r.responseHeaders && (r.responseHeaders.match(/content-length:\s*(\d+)/i) || [])[1]; if (cl) { item.size = Number(cl); if (IS_TOP) ui.refreshRow(item); } }).catch(() => {}); }, download(item) { const name = (sanitizeName(item.title) || fileNameFromUrl(item.url)) + (item.ext ? '.' + item.ext.replace(/^\./, '') : ''); ui.toast(`开始下载:${name.slice(0, 40)}`); const doBlob = () => { gmXhr({ method: 'GET', url: item.url, responseType: 'blob', headers: { Referer: item.pageOrigin || location.origin }, timeout: 300000 }) .then(r => { const blob = r.response instanceof Blob ? r.response : new Blob([r.response]); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = name; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 60000); ui.toast('已通过浏览器保存:' + name.slice(0, 40)); }) .catch(() => ui.toast('下载失败:无法获取该资源(可能有防盗链或需要登录)', true)); }; if (item.type === 'hls') { doBlob(); return; } try { GM_download({ url: item.url, name, headers: { Referer: item.pageOrigin || location.origin }, onerror: (e) => { const msg = (e && e.error) || ''; if (msg === 'not_whitelisted' || msg === 'cancelled') { ui.toast('已取消或被浏览器拦截,尝试备用方式…'); } doBlob(); }, ontimeout: doBlob, }); } catch (e) { doBlob(); } }, copy(item) { GM_setClipboard(item.url, 'text'); ui.toast('链接已复制'); }, }; /* ================================ UI ================================ */ const ui = { root: null, shadow: null, ball: null, panel: null, listEl: null, tab: 'all', visible: false, badge: null, toastEl: null, _toastTimer: null, init() { if (!IS_TOP || document.getElementById('aix-userjs-root')) return; const start = () => { ui.build(); GM_registerMenuCommand('打开 / 关闭下载面板', () => ui.toggle()); GM_registerMenuCommand('立即嗅探本页媒体', () => { handlers.run(); sniffer.domScan(); ui.toast('嗅探完成'); }); }; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start, { once: true }); else start(); }, build() { ui.root = document.createElement('div'); ui.root.id = 'aix-userjs-root'; ui.shadow = ui.root.attachShadow({ mode: 'open' }); ui.shadow.innerHTML = `
AIX 媒体嗅探下载器
×
全部
视频
音频
图片
🔍 嗅探本页
⬇ 下载当前列表
🗑 清空
`; document.documentElement.appendChild(ui.root); ui.ball = ui.shadow.getElementById('ball'); ui.panel = ui.shadow.getElementById('panel'); ui.listEl = ui.shadow.getElementById('list'); ui.badge = ui.shadow.getElementById('badge'); ui.toastEl = ui.shadow.getElementById('toast'); /* 悬浮球:点击开面板,长按/拖动移动 */ let downX = 0, downY = 0, moved = false; ui.ball.addEventListener('mousedown', (e) => { downX = e.clientX; downY = e.clientY; moved = false; }); ui.ball.addEventListener('mousemove', (e) => { if (e.buttons === 1 && (Math.abs(e.clientX - downX) > 4 || Math.abs(e.clientY - downY) > 4)) { moved = true; const r = ui.ball.getBoundingClientRect(); ui.ball.style.left = (e.clientX - r.width / 2) + 'px'; ui.ball.style.top = (e.clientY - r.height / 2) + 'px'; ui.ball.style.right = 'auto'; ui.ball.style.bottom = 'auto'; } }); ui.ball.addEventListener('click', () => { if (!moved) ui.toggle(); }); ui.panel.querySelector('.x').addEventListener('click', () => ui.toggle()); ui.panel.querySelectorAll('.tab').forEach(t => t.addEventListener('click', () => { ui.panel.querySelectorAll('.tab').forEach(x => x.classList.remove('on')); t.classList.add('on'); ui.tab = t.dataset.tab; ui.render(); })); ui.panel.querySelectorAll('.op').forEach(o => o.addEventListener('click', () => { const op = o.dataset.op; if (op === 'scan') { handlers.run(); sniffer.domScan(); ui.toast('嗅探完成'); } if (op === 'clear') { store.clear(); ui.toast('列表已清空'); } if (op === 'dlall') { const items = store.list(ui.tab); if (!items.length) return ui.toast('当前列表为空'); if (items.length > 20) return ui.toast('条目较多,请逐条下载或先筛选(>20 条不批量)', true); items.forEach((it, i) => setTimeout(() => downloader.download(it), i * 1200)); } })); ui.render(); }, toggle() { if (!ui.panel) return; ui.visible = !ui.visible; ui.panel.style.display = ui.visible ? 'flex' : 'none'; if (ui.visible) { ui.render(); ui.badge.style.display = 'none'; } }, onAdded(item) { if (!ui.ball) return; if (!ui.visible) { const n = store.order.length; ui.badge.textContent = n > 99 ? '99+' : n; ui.badge.style.display = 'block'; } else { ui.render(); } }, render() { if (!ui.listEl) return; const items = store.list(ui.tab); ui.listEl.innerHTML = ''; if (!items.length) { const e = document.createElement('div'); e.id = 'empty'; e.textContent = ui.tab === 'all' ? '暂未发现媒体资源,试试「嗅探本页」或播放视频' : '该分类下暂无资源'; ui.listEl.appendChild(e); return; } items.slice(0, 200).forEach(it => ui.listEl.appendChild(ui.row(it))); }, row(item) { const el = document.createElement('div'); el.className = 'item'; el.dataset.id = item.id; const cls = item.type === 'video' ? 'v' : item.type === 'audio' ? 'a' : item.type === 'image' ? 'i' : 'h'; const label = item.type === 'video' ? '视频' : item.type === 'audio' ? '音频' : item.type === 'image' ? '图片' : 'HLS'; const sizeStr = fmtSize(item.size) || (item.size === null && item.type !== 'image' ? '' : ''); el.innerHTML = `
${label}
${[item.quality, sizeStr, hostOf(item.url)].filter(Boolean).join(' · ')}
`; el.querySelector('.t').textContent = item.title || fileNameFromUrl(item.url); el.querySelector('[data-a=dl]').addEventListener('click', () => downloader.download(item)); el.querySelector('[data-a=cp]').addEventListener('click', () => downloader.copy(item)); el.addEventListener('mouseenter', () => downloader.fetchSize(item)); if (item.type !== 'image') downloader.fetchSize(item); return el; }, refreshRow(item) { if (!ui.listEl) return; const el = ui.listEl.querySelector(`[data-id="${item.id}"]`); if (!el) return; const m = el.querySelector('.m'); m.textContent = [item.quality, fmtSize(item.size), hostOf(item.url)].filter(Boolean).join(' · '); }, toast(msg, warn) { if (!ui.toastEl) return; ui.toastEl.textContent = msg; ui.toastEl.style.background = warn ? 'rgba(200,60,60,.94)' : 'rgba(30,32,40,.92)'; ui.toastEl.style.display = 'block'; clearTimeout(ui._toastTimer); ui._toastTimer = setTimeout(() => { ui.toastEl.style.display = 'none'; }, 2600); }, }; /* ================================ 启动 ================================ */ sniffer.start(); forwardToTop(); ui.init(); const boot = () => { sniffer.domScan(); handlers.run(); setTimeout(() => { sniffer.domScan(); handlers.run(); }, 3000); // SPA/懒加载兜底 }; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot, { once: true }); else boot(); })();