// ==UserScript== // @name 网页结构采集器(通用) // @namespace https://local.cxbank.icu/probe // @version 1.0.0 // @description 任意网页点一下,把页面结构 / 点选元素打包上传,用来给新网站做适配。只读。 // @author ZZ // @match *://*/* // @grant none // @run-at document-idle // @license MIT // ==/UserScript== (function () { 'use strict'; var API = 'https://api.cxbank.icu'; var VERSION = '1.0.0'; var MAX_NODES = 1500; var MAX_CLASSES = 150; var MAX_BYTES = 350 * 1024; var out = { meta: {}, picks: [], videos: [], audios: [], iframes: [], inputs: [], buttons: [], forms: [], tables: [], dialogs: [], keywords: [], classes: {}, ids: [], outline: [], errors: [], }; var cls = function (el) { try { return String(el.className || '').trim().slice(0, 180); } catch (e) { return ''; } }; var txt = function (el, n) { try { return String(el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, n || 60); } catch (e) { return ''; } }; function sel(el) { if (!el || !el.tagName) return ''; if (el.id) return '#' + el.id; var c = cls(el).split(/\s+/).filter(Boolean).slice(0, 2); if (c.length) return el.tagName.toLowerCase() + '.' + c.join('.'); return el.tagName.toLowerCase(); } function path(el) { var arr = [], n = el; for (var i = 0; i < 6 && n && n.tagName; i++) { arr.unshift(sel(n) + (n.getAttribute && n.getAttribute('onclick') ? '[onclick]' : '')); n = n.parentElement; } return arr.join(' > '); } function reset() { ['picks', 'videos', 'audios', 'iframes', 'inputs', 'buttons', 'forms', 'tables', 'dialogs', 'keywords', 'ids', 'outline', 'errors'].forEach(function (k) { out[k] = []; }); out.classes = {}; } /* ---------- 结构骨架 ---------- */ var SKIP = { SCRIPT: 1, STYLE: 1, NOSCRIPT: 1, PATH: 1, DEFS: 1, META: 1, LINK: 1, HEAD: 1 }; function walk(el, depth, ctx) { if (!el || !el.tagName || SKIP[el.tagName]) return; if (out.outline.length >= MAX_NODES) return; if (depth > 7) return; var c = cls(el); var interesting = !!el.id || !!c || ['VIDEO', 'AUDIO', 'IFRAME', 'INPUT', 'BUTTON', 'FORM', 'TABLE', 'UL', 'OL', 'LI', 'CANVAS', 'SVG', 'TEXTAREA'].indexOf(el.tagName) >= 0; if (interesting) { out.outline.push({ d: depth, s: sel(el) + (c ? '.' + c.split(/\s+/).slice(0, 3).join('.') : ''), t: txt(el, 40), n: el.children ? el.children.length : 0, }); } if (el.children) { for (var i = 0; i < el.children.length; i++) { walk(el.children[i], depth + 1, ctx); } } } /* ---------- 各类元素 ---------- */ function scanMedia(doc) { try { doc.querySelectorAll('video').forEach(function (v) { out.videos.push({ src: String(v.currentSrc || v.src || '').slice(0, 160), dur: Math.round(v.duration || 0), now: Math.round(v.currentTime || 0), paused: v.paused, rate: v.playbackRate, muted: v.muted, w: v.offsetWidth, h: v.offsetHeight, path: path(v), }); }); doc.querySelectorAll('audio').forEach(function (a) { out.audios.push({ src: String(a.currentSrc || a.src || '').slice(0, 140), path: path(a) }); }); doc.querySelectorAll('iframe,frame').forEach(function (f) { out.iframes.push({ src: String(f.src || '').slice(0, 160), id: String(f.id || ''), cls: cls(f), path: path(f) }); }); } catch (e) { out.errors.push('media:' + e.message); } } function scanControls(doc) { try { doc.querySelectorAll('input,textarea,select').forEach(function (el) { if (out.inputs.length >= 60) return; out.inputs.push({ tag: el.tagName.toLowerCase(), type: String(el.type || ''), name: String(el.name || ''), id: String(el.id || ''), ph: String(el.placeholder || '').slice(0, 40), cls: cls(el), path: path(el), }); }); doc.querySelectorAll('button,[role=button],a.btn,.btn,input[type=submit],input[type=button]').forEach(function (el) { if (out.buttons.length >= 60) return; out.buttons.push({ tag: el.tagName.toLowerCase(), text: txt(el, 30), cls: cls(el), id: String(el.id || ''), type: String(el.type || ''), path: path(el), }); }); doc.querySelectorAll('form').forEach(function (f) { if (out.forms.length >= 12) return; out.forms.push({ action: String(f.action || '').slice(0, 120), method: String(f.method || ''), inputs: f.querySelectorAll('input,textarea').length, path: path(f), }); }); doc.querySelectorAll('table').forEach(function (t) { if (out.tables.length >= 8) return; var ths = []; t.querySelectorAll('th').forEach(function (th) { if (ths.length < 8) ths.push(txt(th, 20)); }); out.tables.push({ cls: cls(t), headers: ths, rows: t.querySelectorAll('tr').length, path: path(t) }); }); } catch (e) { out.errors.push('controls:' + e.message); } } function scanOverlays(doc) { try { var els = doc.querySelectorAll('div,section,aside'); for (var i = 0; i < els.length && out.dialogs.length < 15; i++) { var el = els[i], cs; try { cs = getComputedStyle(el); } catch (e) { continue; } var z = parseInt(cs.zIndex, 10); if ((cs.position === 'fixed' || cs.position === 'absolute') && z > 500 && el.offsetWidth > 180 && el.offsetHeight > 60) { out.dialogs.push({ cls: cls(el), z: z, size: el.offsetWidth + 'x' + el.offsetHeight, text: txt(el, 60), path: path(el) }); } } } catch (e) {} } /* ---------- 关键词:题目 / 课程 / 播放器 / 验证码 ---------- */ var KW = /题目|试题|选项|答案|答题|作业|考试|测验|章节|课程|课时|播放|视频|进度|任务点|已完成|验证码|登录|提交|保存/; function scanKeywords(doc) { try { var all = doc.querySelectorAll('*'); for (var i = 0; i < all.length && i < 8000; i++) { var el = all[i]; if (el.children.length > 6) continue; var t = txt(el, 40); if (!t || t.length < 2) continue; if (!KW.test(t)) continue; if (out.keywords.length >= 60) break; out.keywords.push({ tag: el.tagName.toLowerCase(), cls: cls(el), text: t, path: path(el) }); } } catch (e) {} } function scanClasses(doc) { try { var all = doc.querySelectorAll('*'); for (var i = 0; i < all.length && i < 8000; i++) { var c = cls(all[i]); if (!c) continue; var parts = c.split(/\s+/); for (var j = 0; j < parts.length; j++) { var t = parts[j]; if (t.length < 3 || t.length > 40) continue; out.classes[t] = (out.classes[t] || 0) + 1; } } var entries = Object.keys(out.classes).map(function (k) { return [k, out.classes[k]]; }); entries.sort(function (a, b) { return b[1] - a[1]; }); var top = {}; entries.slice(0, MAX_CLASSES).forEach(function (e) { top[e[0]] = e[1]; }); out.classes = top; } catch (e) {} try { var ids = []; var all2 = doc.querySelectorAll('[id]'); for (var k = 0; k < all2.length && ids.length < 80; k++) ids.push(all2[k].id); out.ids = ids; } catch (e) {} } /* ---------- 框架识别 ---------- */ function framework() { var f = []; try { if (window.Vue) f.push('Vue' + (window.Vue.version ? ' ' + window.Vue.version : '')); if (window.React || document.querySelector('[data-reactroot],#root,#__next')) f.push('React'); if (window.jQuery || window.$) f.push('jQuery'); if (document.querySelector('[class*=el-]')) f.push('ElementUI'); if (document.querySelector('[class*=ant-]')) f.push('AntDesign'); if (document.querySelector('[class*=van-]')) f.push('Vant'); if (document.querySelector('[class*=layui-]')) f.push('LayUI'); if (document.querySelector('[class*=vjs-]')) f.push('video.js'); if (window.player || window.videoPlayer) f.push('player对象'); } catch (e) {} return f; } function collect() { out.meta = { url: String(location.href).slice(0, 300), title: String(document.title || '').slice(0, 120), ready: document.readyState, ua: String(navigator.userAgent || '').slice(0, 160), screen: screen.width + 'x' + screen.height, viewport: window.innerWidth + 'x' + window.innerHeight, at: new Date().toLocaleString('zh-CN'), probeVer: VERSION, frameworks: framework(), lang: String(document.documentElement.lang || ''), }; reset(); scanMedia(document); scanControls(document); scanOverlays(document); scanKeywords(document); scanClasses(document); walk(document.body, 0, {}); return out; } /* ---------- 点选元素 ---------- */ var picking = false; var pickHint = null; function startPick() { picking = true; if (pickHint) pickHint.style.display = 'block'; document.body.style.cursor = 'crosshair'; say('点选模式:点页面上任意元素,会记录它的选择器和结构(Esc 取消)'); } function stopPick() { picking = false; if (pickHint) pickHint.style.display = 'none'; document.body.style.cursor = ''; } document.addEventListener('click', function (e) { if (!picking) return; e.preventDefault(); e.stopPropagation(); var el = e.target; var info = { tag: el.tagName ? el.tagName.toLowerCase() : '', id: String(el.id || ''), cls: cls(el), text: txt(el, 60), path: path(el), kids: el.children ? Array.prototype.slice.call(el.children, 0, 8).map(function (c) { return c.tagName.toLowerCase() + '.' + cls(c).split(/\s+/).slice(0, 2).join('.'); }) : [], attrs: {}, }; ['type', 'name', 'href', 'src', 'onclick', 'role', 'aria-label', 'data-id'].forEach(function (k) { var v = el.getAttribute && el.getAttribute(k); if (v) info.attrs[k] = String(v).slice(0, 80); }); out.picks.push(info); say('已记录:' + info.tag + (info.id ? '#' + info.id : '') + (info.cls ? '.' + info.cls.split(/\s+/)[0] : '')); stopPick(); renderPre(); }, true); document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && picking) { stopPick(); say('已取消点选'); } }, true); /* ---------- 界面 ---------- */ var box = document.createElement('div'); box.style.cssText = 'position:fixed;right:16px;bottom:110px;z-index:2147483647;' + 'font:13px/1.6 -apple-system,"Microsoft YaHei",sans-serif;background:rgba(255,255,255,.96);' + 'color:#1c1c1e;border:.5px solid rgba(0,0,0,.1);border-radius:14px;' + 'box-shadow:0 10px 30px rgba(0,0,0,.18);padding:12px 14px;max-width:310px;display:none;' + '-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px)'; box.innerHTML = '
网页结构采集 v1.0
' + '
采集整页结构,或点选某个元素。
' + '' + '' + ''; var fab = document.createElement('button'); fab.textContent = '采'; fab.title = '网页结构采集 v1.0'; fab.style.cssText = 'position:fixed;right:16px;bottom:56px;width:42px;height:42px;border-radius:50%;' + 'border:none;cursor:pointer;z-index:2147483647;background:linear-gradient(145deg,#0a84ff,#0071e3);' + 'color:#fff;font:600 14px/1 -apple-system,sans-serif;box-shadow:0 8px 20px rgba(0,122,255,.36)'; fab.onclick = function () { box.style.display = box.style.display === 'none' ? 'block' : 'none'; }; pickHint = document.createElement('div'); pickHint.textContent = '点选模式中…(Esc 取消)'; pickHint.style.cssText = 'position:fixed;left:50%;top:16px;transform:translateX(-50%);z-index:2147483647;' + 'display:none;background:#007aff;color:#fff;padding:7px 16px;border-radius:99px;' + 'font:600 13px/1.3 -apple-system,sans-serif;box-shadow:0 6px 20px rgba(0,122,255,.4)'; document.body.appendChild(fab); document.body.appendChild(box); document.body.appendChild(pickHint); var msg = box.querySelector('#pmsg'); var pre = box.querySelector('#ppre'); var go = box.querySelector('#pgo'); var pick = box.querySelector('#ppick'); function say(t, c) { msg.textContent = t; msg.style.color = c || '#8e8e93'; } function renderPre() { try { pre.style.display = ''; pre.textContent = '已点选 ' + out.picks.length + ' 个元素:\n' + out.picks.map(function (p) { return '· ' + p.path; }).join('\n'); } catch (e) {} } pick.onclick = function () { startPick(); }; go.onclick = async function () { go.disabled = true; say('正在采集…'); var text = ''; try { var data = collect(); text = JSON.stringify(data, null, 1); if (text.length > MAX_BYTES) text = text.slice(0, MAX_BYTES) + '\n…(截断)'; pre.style.display = ''; pre.textContent = text.slice(0, 1200); say('采集完成 ' + Math.round(text.length / 1024) + ' KB:元素 ' + (data.outline || []).length + ',视频 ' + (data.videos || []).length + ',按钮 ' + (data.buttons || []).length + ',点选 ' + (data.picks || []).length + ',上传中…'); } catch (e) { say('采集失败:' + e.message, '#dc2626'); go.disabled = false; return; } try { var r = await fetch(API + '/diag/upload', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text: text, reason: '通用页面采集', ver: VERSION, page: String(location.href).slice(0, 120), }), }); var j = await r.json(); if (j && j.ok) { try { await navigator.clipboard.writeText(j.id); } catch (e) {} say('✔ 上传成功,ID 已复制:' + j.id, '#248a3d'); } else { say('上传失败:' + ((j && j.reason) || '未知'), '#dc2626'); } } catch (e) { say('上传失败:' + e.message + '\n内容在下面,可复制', '#dc2626'); } go.disabled = false; }; setTimeout(function () { box.style.display = 'block'; }, 900); })();