// ==UserScript== // @name 学科网自动下载 · 猫爪脚本 // @name:zh-CN 学科网自动下载 · 猫爪脚本 // @namespace https://github.com/qoderwork/zxxk-autodownload // @version 1.2.0 // @description 在学科网 / AI职教(*.xkw.com、*.zxxk.com)的打包页、资源篮、资源详情页、列表页采集资料链接。按“本课件/同专辑/推荐/热门/打包/资源篮/整包”分类,本课件优先默认勾选,打包/资源篮/整包整组默认勾选,推荐/热门/同专辑类默认不选。批量提交到下载服务,自动下载文件或标记“已发送到邮箱”;额度触顶横幅提示与自动暂停;文件名冲突自动加资料ID;支持采集所属专辑整包。 // @author QoderWork // @match *://*.xkw.com/* // @match *://*.zxxk.com/* // @match *://xkw.com/* // @match *://zxxk.com/* // @run-at document-idle // @noframes // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @connect * // @license MIT // ==/UserScript== (function () { "use strict"; /* ============ 存储与默认配置 ============ */ var PREFIX = "zxxk_dl_"; function sget(k, def) { try { var v = GM_getValue(PREFIX + k); return v === undefined ? def : v; } catch (e) { return def; } } function sset(k, v) { try { GM_setValue(PREFIX + k, v); } catch (e) {} } var DEFAULTS = { server: "http://8.136.213.77:8080/SPM", username: "", password: "", email: "", delay: 4, autoDownload: true, uniqueFilename: true // 同名文件自动追加资料ID避免覆盖 }; var CFG = { server: sget("server", DEFAULTS.server).replace(/\/+$/, ""), username: sget("username", DEFAULTS.username), password: sget("password", DEFAULTS.password), email: sget("email", DEFAULTS.email), delay: parseFloat(sget("delay", DEFAULTS.delay)) || DEFAULTS.delay, autoDownload: sget("autoDownload", DEFAULTS.autoDownload) !== false, uniqueFilename: sget("uniqueFilename", DEFAULTS.uniqueFilename) !== false }; /* ============ 下载服务响应码 → 文案 ============ */ var CODE_MSG = { "111": "账号或者密码输入错误", "222": "当天下载份数达到上限,请明天再试", "555": "本周下载份数达到上限,请下周一再试", "666": "当前材料为第三方,无权下载", "777": "本周剩余份数不足以下载本书", "888": "网址输入错误 / 无法解析", "1010": "出现异常,请刷新重试", "1020": "邮箱输入错误", "1030": "多个网址请分批次提交,请勿一次性提交", "1040": "当前账号未激活,请联系客服", "1050": "份数已全部用完,请重新续费", "1060": "获取下载记录异常,请重试", "1070": "当前账号已到期,请重新续费", "1080": "剩余份数不足以下载当前材料", "1090": "该网址已提交过,请勿重复提交" }; // 额度/份数相关:命中即在顶部横幅提示 var LIMIT_CODES = { "222": 1, "555": 1, "777": 1, "1050": 1, "1070": 1, "1080": 1 }; // 硬上限/账号状态:命中则自动暂停整个队列 var HARD_STOP_CODES = { "222": 1, "555": 1, "1040": 1, "1050": 1, "1070": 1 }; /* ============ 类别定义 ============ */ var CATS = { self: { label: "本课件", color: "#c0392b", checked: true }, same_album: { label: "同专辑", color: "#1a56db", checked: false }, recommend: { label: "推荐", color: "#7a5cff", checked: false }, hot: { label: "热门", color: "#b26a00", checked: false }, pack: { label: "打包资料", color: "#1d8348", checked: true }, cart: { label: "资源篮", color: "#0d7a6f", checked: true }, album: { label: "整包", color: "#0a6aa1", checked: true }, list: { label: "页面资源", color: "#555", checked: false } }; var CAT_ORDER = ["self", "same_album", "album", "pack", "cart", "recommend", "hot", "list"]; /* ============ 队列 ============ */ // item: { url, id, title, cat, status, link, msg, checked } var queue = sget("queue", []); try { if (typeof queue === "string") queue = JSON.parse(queue); } catch (e) { queue = []; } if (!Array.isArray(queue)) queue = []; var collapsed = sget("collapsed", {}); try { if (typeof collapsed === "string") collapsed = JSON.parse(collapsed); } catch (e) { collapsed = {}; } if (!collapsed || typeof collapsed !== "object") collapsed = {}; var running = false; var stopFlag = false; var usedNames = {}; // 每次 processQueue 重置 /* ============ 通用工具 ============ */ function esc(s) { return String(s == null ? "" : s).replace( /[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; } ); } function parseResp(text) { var d = (text || "").trim(); var i = d.indexOf(":"); if (i < 0) return { code: d, rest: "" }; return { code: d.substring(0, i), rest: d.substring(i + 1) }; } function splitRest(rest) { var i = rest.indexOf(":"); if (i < 0) return { name: rest.trim(), payload: "" }; return { name: rest.substring(0, i).trim(), payload: rest.substring(i + 1).trim() }; } function safeName(s, fallback) { var n = (s || "").trim().replace(/[\\/:*?"<>|\r\n]+/g, "_").replace(/\s+/g, " "); return n || fallback || "zxxk-file"; } function docIdFromUrl(url) { var m = /\/doc\/(\d+)\.html?/i.exec(url || ""); return m ? m[1] : ""; } function uniqueFilename(name, itemId) { var full = (name || "zxxk-file").trim(); if (!CFG.uniqueFilename) return full; if (!usedNames[full.toLowerCase()]) { usedNames[full.toLowerCase()] = 1; return full; } var m = /^(.*?)(\.[^.]*)$/.exec(full) || [null, full, ""]; var base = m[1], ext = m[2] || ""; var id = itemId || ""; var attempts = [ id ? base + "_" + id + ext : null, id ? base + "_" + id + "_2" + ext : base + "_2" + ext, id ? base + "_" + id + "_3" + ext : base + "_3" + ext, base + "_" + Date.now() + ext ]; for (var i = 0; i < attempts.length; i++) { var cand = attempts[i]; if (cand && !usedNames[cand.toLowerCase()]) { usedNames[cand.toLowerCase()] = 1; return cand; } } return full; } function log(msg) { var el = document.getElementById("zxxk-log"); if (!el) return; var t = new Date().toLocaleTimeString("zh-CN", { hour12: false }); var line = document.createElement("div"); line.className = "zxxk-log-line"; line.textContent = "[" + t + "] " + msg; el.appendChild(line); while (el.childNodes.length > 400) el.removeChild(el.firstChild); el.scrollTop = el.scrollHeight; } /* ============ 顶部横幅(方案 A:额度触顶被动提示) ============ */ function showBanner(text, tone) { var box = document.getElementById("zxxk-banner"); if (!box) return; var txt = document.getElementById("zxxk-banner-text"); if (txt) txt.textContent = text; box.className = "zxxk-banner show " + (tone || "warn"); box.style.display = "block"; } function hideBanner() { var el = document.getElementById("zxxk-banner"); if (el) { el.style.display = "none"; } } /* ============ GM_xmlhttpRequest 封装 ============ */ function gmPost(url, body) { return new Promise(function (resolve, reject) { GM_xmlhttpRequest({ method: "POST", url: url, headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }, data: body, timeout: 120000, onload: function (r) { resolve(r.responseText || ""); }, onerror: function () { reject(new Error("网络请求失败")); }, ontimeout: function () { reject(new Error("请求超时")); } }); }); } function gmGetText(url) { return new Promise(function (resolve, reject) { GM_xmlhttpRequest({ method: "GET", url: url, headers: { Referer: location.href }, timeout: 60000, onload: function (r) { resolve(r.responseText || ""); }, onerror: function () { reject(new Error("拉取失败")); }, ontimeout: function () { reject(new Error("拉取超时")); } }); }); } function gmGetBlob(url, extraHeaders) { return new Promise(function (resolve, reject) { GM_xmlhttpRequest({ method: "GET", url: url, responseType: "arraybuffer", headers: Object.assign({ Referer: location.href }, extraHeaders || {}), timeout: 300000, onload: function (r) { resolve(r); }, onerror: function () { reject(new Error("文件下载失败")); }, ontimeout: function () { reject(new Error("文件下载超时")); } }); }); } function filenameFromHeaders(headers) { if (!headers) return ""; var m = /filename\*?=\s*(?:UTF-8'')?["']?([^"';\n]+)["']?/i.exec(headers); if (m) { try { return decodeURIComponent(m[1].trim()); } catch (e) { return m[1].trim(); } } return ""; } function saveBlob(arrayBuffer, filename) { var blob = new Blob([arrayBuffer]); var objUrl = URL.createObjectURL(blob); var a = document.createElement("a"); a.href = objUrl; a.download = filename || "zxxk-" + Date.now(); document.body.appendChild(a); a.click(); setTimeout(function () { document.body.removeChild(a); URL.revokeObjectURL(objUrl); }, 2000); } function downloadDirect(fileUrl, title, itemId) { return gmGetBlob(fileUrl).then(function (r) { var raw = filenameFromHeaders(r.responseHeaders) || safeName(title, "zxxk-" + Date.now()); var name = uniqueFilename(raw, itemId); saveBlob(r.response, name); return name; }); } function downloadJiaofu(path, name, itemId) { var url = CFG.server + "/DownloadJiaofu"; return new Promise(function (resolve, reject) { GM_xmlhttpRequest({ method: "POST", url: url, headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }, data: "path=" + encodeURIComponent(encodeURI(path)), responseType: "arraybuffer", timeout: 300000, onload: function (r) { var guessName = filenameFromHeaders(r.responseHeaders) || name || (function () { var b = path.lastIndexOf("\\"); return b >= 0 ? path.substring(b + 1) : path; })(); var fname = uniqueFilename(guessName, itemId); saveBlob(r.response, fname); resolve(fname); }, onerror: function () { reject(new Error("教材下载失败")); }, ontimeout: function () { reject(new Error("教材下载超时")); } }); }); } /* ============ 单条提交 ============ */ function submitOne(item) { var body = "username=" + CFG.username + "&password=" + CFG.password + "&zxxkurl=" + item.url + "&usermail=" + CFG.email; return gmPost(CFG.server + "/zxxkdown", body).then(function (text) { var p = parseResp(text); var code = p.code, rest = p.rest; if (code === "444" || code === "4443") { var s = splitRest(rest); if (/^\/\//.test(s.payload)) s = { name: "", payload: rest }; item.link = s.payload; if (CFG.autoDownload && s.payload) { return downloadDirect(s.payload, s.name || item.title, item.id) .then(function (fname) { item.status = "已下载"; item.msg = fname; }) .catch(function (err) { item.status = "下载失败"; item.msg = (err && err.message) || "未知错误"; }); } else { item.status = "已解析"; item.msg = "已生成直链,点击“打开”下载"; } return; } if (code === "4442") { var s2 = splitRest(rest); if (CFG.autoDownload && s2.payload) { return downloadJiaofu(s2.payload, s2.name, item.id) .then(function (fname) { item.status = "已下载"; item.msg = fname; }) .catch(function (err) { item.status = "下载失败"; item.msg = (err && err.message) || "未知错误"; }); } else { item.status = "已解析"; item.msg = "教材待下载:" + s2.payload; } return; } if (code === "333" || code === "9333") { var s3 = splitRest(rest); item.status = "已发邮箱"; item.msg = "材料将发送到邮箱,请 10 分钟后查收" + (s3.name ? "(" + s3.name + ")" : ""); return; } // 其它为错误码 item.status = "失败"; var friendly = CODE_MSG[code] || rest || ("未知响应:" + (text || "").slice(0, 60)); item.msg = friendly; if (LIMIT_CODES[code]) { var extra = (rest && rest !== friendly) ? " · " + rest : ""; var tone = (code === "1040" || code === "1050" || code === "1070") ? "fatal" : "warn"; showBanner("⚠ " + friendly + extra, tone); } if (HARD_STOP_CODES[code]) { stopFlag = true; log("命中额度/账号硬上限(码 " + code + "),队列自动暂停。"); } return; }); } /* ============ 队列批处理 ============ */ function processQueue() { if (running) return; var todo = queue.filter(function (it) { return it.checked !== false && (it.status === "待下载" || it.status === "重试"); }); if (!todo.length) { log("没有待下载的条目(先“采集”或勾选条目再点“开始下载”)。"); return; } if (!CFG.username || !CFG.password) { log("请先在“配置”里填写账号与密码。"); toggleConfig(true); return; } running = true; stopFlag = false; usedNames = {}; hideBanner(); setRunningUI(true); log("开始下载,共 " + todo.length + " 条,间隔 " + CFG.delay + " 秒。"); var chain = Promise.resolve(); todo.forEach(function (item) { chain = chain.then(function () { if (stopFlag) return; item.status = "解析中"; item.msg = ""; renderList(); return submitOne(item) .catch(function (err) { item.status = "失败"; item.msg = (err && err.message) || "请求失败"; }) .then(function () { log(item.url + " → " + item.status + (item.msg ? ":" + item.msg : "")); renderList(); if (!stopFlag) return sleep(CFG.delay * 1000); }); }); }); chain.then(function () { running = false; setRunningUI(false); if (stopFlag) log("队列已停止(命中额度/账号硬上限或你手动停止)。"); else log("全部处理完成。"); updateProgress(); }); } function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); } /* ============ 页面类型与专辑识别 ============ */ function detectPageKind() { var p = location.pathname || ""; if (/\/doc\/\d+\.html?/i.test(p)) return "doc"; if (/\/docpack\/\d+\.html?/i.test(p)) return "docpack"; if (/\/cart(\?|$|\/)/i.test(p)) return "cart"; return "other"; } function findAlbumUrl() { var scope = document.querySelector(".album-info-header") || document; var anchors = scope.querySelectorAll("a[href]"); for (var i = 0; i < anchors.length; i++) { var h = anchors[i].href || ""; if (/\/docpack\/\d+/i.test(h) || /\/zhuanti\/\d+/i.test(h) || /\/album\/\d+/i.test(h) || /\/special\/\d+/i.test(h)) return h; } var all = document.querySelectorAll('a[href*="/docpack/"], a[href*="/zhuanti/"], a[href*="/album/"], a[href*="/special/"]'); if (all.length) return all[0].href; return ""; } function findAlbumTotal() { var el = document.querySelector(".album-info-header"); if (!el) return 0; var m = /(\d+)\s*份/.exec(el.textContent || ""); return m ? parseInt(m[1], 10) : 0; } /* ============ 采集:把锚点归入某个类别 ============ */ function addItem(byId, url, title, cat) { var key = (url || "").split("#")[0]; if (byId[key]) return false; var id = docIdFromUrl(key); byId[key] = { url: key, id: id, title: (title || "资料 " + (id || "")).slice(0, 80), cat: cat, status: "待下载", link: "", msg: "", checked: CATS[cat] ? CATS[cat].checked : false }; return true; } function addHrefToMap(href, title, byId, cat, baseUrl) { var m = /\/doc\/(\d+)\.html?/i.exec(href || ""); if (!m) return; var abs = href; if (!/^https?:/i.test(abs)) { try { abs = new URL(abs, baseUrl || location.href).href; } catch (e) { return; } } addItem(byId, abs.split("#")[0], (title || "").trim() || ("资料 " + m[1]), cat); } // 从 DOM(document 或解析出的 HTML doc)扫描,填充 byId function scanAnchors(container, byId, pageKind) { var fallbackCat = pageKind === "docpack" ? "pack" : pageKind === "cart" ? "cart" : pageKind === "doc" ? "list" : "list"; function eachAnchor(sel, cat) { var nodes = container.querySelectorAll ? container.querySelectorAll(sel) : []; Array.prototype.forEach.call(nodes, function (a) { var img = a.querySelector && a.querySelector("img"); var title = (a.textContent || "").trim() || (a.getAttribute && (a.getAttribute("title") || "")) || (img && img.alt) || ""; addHrefToMap(a.href || a.getAttribute("href"), title, byId, cat); }); } // 详情页 / 打包页都可能出现的语义区块 var zones = [ { cat: "same_album", sel: '.document-from-same-album a[href*="/doc/"]' }, { cat: "recommend", sel: '.recommend-document a[href*="/doc/"]' }, { cat: "hot", sel: '.hot-document a[href*="/doc/"]' } ]; zones.forEach(function (z) { eachAnchor(z.sel, z.cat); }); // 兜底:尚未归类的 /doc/ 归入页面性质对应类别 eachAnchor('a[href*="/doc/"]', fallbackCat); } function collectLinks() { var byId = {}; queue.forEach(function (it) { byId[it.url] = it; }); // 已存在的保持不动 var snapshot = {}; // 本次采集结果(去重后) scanAnchors(document, snapshot, detectPageKind()); // 本课件(详情页) if (detectPageKind() === "doc") { var idm = /\/doc\/(\d+)\.html?/i.exec(location.pathname); if (idm) { var selfUrl = location.origin + location.pathname; var selfTitle = (document.title || "本课件") .replace(/\s*[-|·]\s*(AI职教|学科网|zxxk).*$/i, "").trim() || "本课件"; addItem(snapshot, selfUrl, selfTitle, "self"); } } var added = 0; var perCat = {}; Object.keys(snapshot).forEach(function (k) { if (!byId[k]) { queue.push(snapshot[k]); byId[k] = snapshot[k]; added++; var c = snapshot[k].cat; perCat[c] = (perCat[c] || 0) + 1; } }); sset("queue", JSON.stringify(queue)); renderList(); if (added === 0) { log("当前页面未发现可采集的新链接。若为懒加载页,请滚动加载后再采集,或点“采集整包”。"); } else { var parts = CAT_ORDER.filter(function (c) { return perCat[c]; }) .map(function (c) { return CATS[c].label + " " + perCat[c]; }); log("采集新增 " + added + " 条:" + parts.join(",") + "。队列共 " + queue.length + " 条。"); } } /* ============ 采集整包(所属专辑全部) ============ */ function collectAlbumAll() { var pageKind = detectPageKind(); var targetUrl = pageKind === "docpack" ? location.href : findAlbumUrl(); if (!targetUrl) { log("未在页面找到“所属专辑/打包”链接,无法采集整包。"); return; } log("开始拉取整包:" + targetUrl); gmGetText(targetUrl).then(function (html) { var doc = new DOMParser().parseFromString(html, "text/html"); var byId = {}; queue.forEach(function (it) { byId[it.url] = it; }); var snapshot = {}; // 整包页里的 /doc/ 统一归为 album(避免误标推荐/热门) var anchors = doc.querySelectorAll ? doc.querySelectorAll('a[href*="/doc/"]') : []; Array.prototype.forEach.call(anchors, function (a) { var img = a.querySelector && a.querySelector("img"); var title = (a.textContent || "").trim() || (a.getAttribute && (a.getAttribute("title") || "")) || (img && img.alt) || ""; addHrefToMap(a.getAttribute("href"), title, snapshot, "album", targetUrl); }); var added = 0; Object.keys(snapshot).forEach(function (k) { if (!byId[k]) { queue.push(snapshot[k]); byId[k] = snapshot[k]; added++; } }); sset("queue", JSON.stringify(queue)); renderList(); log("整包采集完成,新增 " + added + " 条,队列共 " + queue.length + " 条。"); }).catch(function (err) { log("整包拉取失败:" + ((err && err.message) || err)); }); } /* ============ UI ============ */ function buildUI() { GM_addStyle( [ "#zxxk-fab{position:fixed;right:18px;bottom:18px;z-index:2147483646;width:52px;height:52px;border-radius:50%;", "background:linear-gradient(135deg,#ff5f6d,#ffc371);color:#fff;font-size:22px;line-height:52px;text-align:center;", "cursor:pointer;box-shadow:0 6px 18px rgba(0,0,0,.28);user-select:none;font-family:-apple-system,Segoe UI,Microsoft YaHei,sans-serif;}", "#zxxk-fab:hover{transform:translateY(-2px);}", "#zxxk-panel{position:fixed;right:18px;bottom:80px;z-index:2147483646;width:460px;max-height:80vh;display:none;", "flex-direction:column;background:#fff;color:#222;border-radius:12px;overflow:hidden;font-size:13px;", "box-shadow:0 12px 40px rgba(0,0,0,.28);font-family:-apple-system,Segoe UI,Microsoft YaHei,sans-serif;border:1px solid #eee;}", "#zxxk-panel.open{display:flex;}", ".zxxk-hd{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:#2b3a55;color:#fff;}", ".zxxk-hd b{font-size:14px;}", ".zxxk-hd .zxxk-hd-btns span{margin-left:12px;cursor:pointer;opacity:.9;}", ".zxxk-body{overflow:auto;padding:10px 12px;}", ".zxxk-banner{display:none;padding:8px 10px;border-radius:8px;font-size:12px;margin-bottom:8px;line-height:1.5;}", ".zxxk-banner.warn{background:#fff4e5;color:#8a4b00;border:1px solid #ffd9a8;}", ".zxxk-banner.fatal{background:#fdecea;color:#a02118;border:1px solid #f5b5ae;}", ".zxxk-banner .close{float:right;cursor:pointer;opacity:.6;margin-left:6px;}", ".zxxk-cfg{display:none;background:#f6f8fb;border:1px solid #e6ebf2;border-radius:8px;padding:10px;margin-bottom:10px;}", ".zxxk-cfg.open{display:block;}", ".zxxk-row{display:flex;align-items:center;margin:6px 0;gap:8px;}", ".zxxk-row label{width:64px;color:#555;flex:none;}", ".zxxk-row input[type=text],.zxxk-row input[type=password]{flex:1;padding:6px 8px;border:1px solid #d4dbe6;border-radius:6px;box-sizing:border-box;font-size:13px;}", ".zxxk-row input[type=number]{width:80px;padding:6px;border:1px solid #d4dbe6;border-radius:6px;}", ".zxxk-inline{display:flex;align-items:center;gap:6px;color:#555;}", ".zxxk-tools{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px;}", ".zxxk-list{border:1px solid #eee;border-radius:8px;max-height:340px;overflow:auto;}", ".zxxk-group{border-bottom:1px solid #f0f0f0;}", ".zxxk-group:last-child{border-bottom:none;}", ".zxxk-group-hd{display:flex;align-items:center;gap:8px;padding:7px 10px;background:#f7f9fc;position:sticky;top:0;z-index:1;}", ".zxxk-group-hd .caret{width:12px;display:inline-block;color:#889;cursor:pointer;}", ".zxxk-group-hd .dot{width:8px;height:8px;border-radius:50%;display:inline-block;}", ".zxxk-group-hd .gname{font-weight:600;color:#334;}", ".zxxk-group-hd .gcount{color:#778;font-size:12px;}", ".zxxk-group-hd .gselall{margin-left:auto;color:#1a73e8;font-size:12px;cursor:pointer;}", ".zxxk-group-bd.hide{display:none;}", ".zxxk-item{display:flex;align-items:center;gap:8px;padding:7px 12px 7px 24px;border-bottom:1px solid #f5f5f5;}", ".zxxk-item:last-child{border-bottom:none;}", ".zxxk-item.self{background:#fff8f7;}", ".zxxk-item .ttl{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#333;}", ".zxxk-item .st{flex:none;font-size:12px;padding:2px 7px;border-radius:10px;background:#eef1f5;color:#667;}", ".zxxk-item .st.ok{background:#e6f6ec;color:#1d8348;}", ".zxxk-item .st.mail{background:#fff3e0;color:#b26a00;}", ".zxxk-item .st.err{background:#fdecea;color:#c0392b;}", ".zxxk-item .st.run{background:#e8f0fe;color:#1a56db;}", ".zxxk-item .op{flex:none;color:#1a73e8;cursor:pointer;margin-left:4px;}", ".zxxk-ft{display:flex;align-items:center;gap:8px;padding:10px 12px;border-top:1px solid #eee;background:#fafbfc;}", "button.zxxk-btn{border:none;border-radius:6px;padding:8px 14px;font-size:13px;cursor:pointer;background:#2b3a55;color:#fff;}", "button.zxxk-btn.ghost{background:#eef1f5;color:#334;}", "button.zxxk-btn.dl{background:linear-gradient(135deg,#ff5f6d,#ff8a5f);}", "button.zxxk-btn.primary{background:linear-gradient(135deg,#c0392b,#e67e22);}", "button.zxxk-btn:disabled{opacity:.5;cursor:not-allowed;}", ".zxxk-log{margin-top:8px;border:1px solid #eee;border-radius:8px;background:#0f1720;color:#b9c6d6;font-size:11px;", "line-height:1.5;padding:8px;height:110px;overflow:auto;font-family:Menlo,Consolas,monospace;}", ".zxxk-prog{margin-left:auto;color:#666;font-size:12px;}", ".zxxk-empty{padding:18px;text-align:center;color:#aaa;}", ".zxxk-self-bar{display:flex;gap:6px;align-items:center;margin-bottom:8px;padding:8px 10px;", "background:linear-gradient(90deg,#fff5f5,#fff9f0);border:1px solid #ffe1d8;border-radius:8px;}", ".zxxk-self-bar .badge{background:#c0392b;color:#fff;border-radius:4px;padding:2px 6px;font-size:11px;flex:none;}", ".zxxk-self-bar .t{flex:1;color:#5b2a26;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}", ".zxxk-self-bar button{padding:6px 10px;font-size:12px;}" ].join("") ); var fab = document.createElement("div"); fab.id = "zxxk-fab"; fab.title = "学科网自动下载"; fab.textContent = "🐾"; document.body.appendChild(fab); var panel = document.createElement("div"); panel.id = "zxxk-panel"; panel.innerHTML = '