// ==UserScript== // @name |🥇PP网课小助手|飘飘|热更新版本| // @namespace 飘飘 // @version 1.2.0 // @author PIAOPIAO // @license MIT // @description 轻量级引导脚本,自动拉取最新代码执行。支持热更新,无需手动更新脚本。 // @icon https://cdn.jsdmirror.com/gh/piaopiaosir/pp-course-assistant@main/icon.webp // @match *://*.chaoxing.com/* // @match *://*.xuexitong.com/* // @match *://*.xueyinonline.com/* // @match *://*.edu.cn/* // @match *://*.nbdlib.cn/* // @match *://*.hnsyu.net/* // @match *://*.gdhkmooc.com/* // @match *://*.sslibrary.com/* // @match *://*.zhihuishu.com/* // @match *://*.hike-teaching-center.polymas.com/* // @match *://*.yuketang.cn/* // @match *://*.gdufemooc.cn/* // @require https://lib.baomitu.com/vue/3.5.0/vue.global.prod.js // @require https://lib.baomitu.com/vue-demi/0.14.7/index.iife.js // @require data:application/javascript,window.Vue%3DVue%3B // @require https://lib.baomitu.com/element-plus/2.8.2/index.full.min.js // @require https://lib.baomitu.com/pinia/2.3.1/pinia.iife.min.js // @require https://lib.baomitu.com/rxjs/7.8.2/rxjs.umd.min.js // @require https://lib.baomitu.com/blueimp-md5/2.19.0/js/md5.min.js // @resource ElementPlusStyle https://lib.baomitu.com/element-plus/2.8.2/index.min.css // @resource fontDecryptLib https://cdn.jsdmirror.com/gh/piaopiaosir/pp-font-decrypt@v1.0.3/font-decrypt.js // @connect scriptcat.org // @connect * // @grant GM_addStyle // @grant GM_getResourceText // @grant GM_getValue // @grant GM_info // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant unsafeWindow // @run-at document-start // @tag ChatGPT|Gemini|Deepseek多模型支持 // @tag 免费题库 // @tag 全题型支持 // ==/UserScript== (function () { 'use strict'; const CONFIG = { VERSION_SERVER: { VERSION_URL: 'https://scriptcat.org/scripts/code/5597/%7C%F0%9F%A5%87PP%E7%BD%91%E8%AF%BE%E5%B0%8F%E5%8A%A9%E6%89%8B%7C%E9%A3%98%E9%A3%98%7C.meta.js', BOOTSTRAP_VERSION_URL: 'https://scriptcat.org/scripts/code/5615/%7C%F0%9F%A5%87PP%E7%BD%91%E8%AF%BE%E5%B0%8F%E5%8A%A9%E6%89%8B%7C%E9%A3%98%E9%A3%98%7C%E7%83%AD%E6%9B%B4%E6%96%B0%E7%89%88%E6%9C%AC%7C.meta.js', }, SCRIPTCAT: { SCRIPT_ID: '5597', SCRIPT_NAME: 'PP网课小助手', CODE_URL: 'https://scriptcat.org/scripts/code/5597/%7C%F0%9F%A5%87PP%E7%BD%91%E8%AF%BE%E5%B0%8F%E5%8A%A9%E6%89%8B%7C%E9%A3%98%E9%A3%98%7C.user.js' }, SOURCE: { GH_PROXY_URL: 'https://gh-proxy.com/https://raw.githubusercontent.com/piaopiaosir/pp-course-assistant/main/script.user.js', CDN_URL: 'https://cdn.jsdmirror.com/gh/piaopiaosir/pp-course-assistant@main/script.user.js' }, CACHE_KEY: 'cloud_script_cache', VERSION_KEY: 'cloud_script_version', LAST_CHECK_KEY: 'last_check_time' }; const log = { info: (msg, ...args) => console.log(`%c[云端脚本] ${msg}`, 'color: #667eea; font-weight: bold;', ...args), success: (msg, ...args) => console.log(`%c[云端脚本] ✓ ${msg}`, 'color: #10b981; font-weight: bold;', ...args), warn: (msg, ...args) => console.warn(`%c[云端脚本] ⚠ ${msg}`, 'color: #f59e0b; font-weight: bold;', ...args), error: (msg, ...args) => console.error(`%c[云端脚本] ✗ ${msg}`, 'color: #ef4444; font-weight: bold;', ...args) }; let loadingWindow = null; function showLoadingWindow() { if (loadingWindow) return; const createLoading = () => { loadingWindow = document.createElement('div'); loadingWindow.id = 'cloud-script-loading-window'; loadingWindow.innerHTML = `
脚本加载中...
`; loadingWindow.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; animation: toastSlideIn 0.3s ease; `; const style = document.createElement('style'); style.id = 'cloud-script-loading-styles'; style.textContent = ` #cloud-script-loading-window .loading-content { background: #ffffff; border: 1px solid #e5e7eb; border-left: 3px solid #0052D9; padding: 10px 16px; border-radius: 6px; display: flex; align-items: center; gap: 10px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1), 0 1px 4px rgba(0, 0, 0, 0.06); min-width: 180px; } #cloud-script-loading-window .loading-spinner { width: 18px; height: 18px; border: 2px solid #e5e7eb; border-top-color: #0052D9; border-right-color: #0052D9; border-radius: 50%; animation: loadingSpin 0.8s linear infinite; flex-shrink: 0; display: block; } #cloud-script-loading-window .loading-text { color: #374151; font-size: 13px; font-weight: 500; white-space: nowrap; } @keyframes loadingSpin { to { transform: rotate(360deg); } } @keyframes toastSlideIn { from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: translateX(0); } } `; document.head.appendChild(style); document.body.appendChild(loadingWindow); log.info('加载窗口已显示'); }; if (document.body) { createLoading(); } else { document.addEventListener('DOMContentLoaded', createLoading); } } function hideLoadingWindow() { if (loadingWindow) { loadingWindow.style.opacity = '0'; loadingWindow.style.transition = 'opacity 0.25s ease'; setTimeout(() => { if (loadingWindow) { loadingWindow.remove(); loadingWindow = null; const style = document.getElementById('cloud-script-loading-styles'); if (style) style.remove(); log.info('加载窗口已关闭'); } }, 250); } } window.__closeLoadingWindow__ = hideLoadingWindow; function compareVersion(v1, v2) { const parts1 = v1.split('.').map(Number); const parts2 = v2.split('.').map(Number); for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { const p1 = parts1[i] || 0; const p2 = parts2[i] || 0; if (p1 > p2) return 1; if (p1 < p2) return -1; } return 0; } function httpGet(url, isText = false, headers = {}, timeout = 15000) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, headers: headers, timeout: timeout, onload: (response) => { if (response.status === 200) { resolve(isText ? response.responseText : response.response); } else { reject(new Error(`HTTP ${response.status}`)); } }, onerror: () => reject(new Error('网络请求失败')), ontimeout: () => reject(new Error('请求超时')) }); }); } async function fetchRangeText(url, range, timeout) { const controller = typeof AbortController === 'function' ? new AbortController() : null; const timer = controller ? setTimeout(() => controller.abort(), timeout) : null; try { const response = await fetch(url, { method: 'GET', headers: { Range: range }, cache: 'no-store', ...(controller ? { signal: controller.signal } : {}) }); if (response.status !== 200 && response.status !== 206) { throw new Error(`HTTP ${response.status}`); } return await response.text(); } finally { if (timer) clearTimeout(timer); } } function getLocalCache() { return { version: GM_getValue(CONFIG.VERSION_KEY, ''), code: GM_getValue(CONFIG.CACHE_KEY, ''), lastCheck: GM_getValue(CONFIG.LAST_CHECK_KEY, 0) }; } function saveLocalCache(version, code) { GM_setValue(CONFIG.VERSION_KEY, version); GM_setValue(CONFIG.CACHE_KEY, code); GM_setValue(CONFIG.LAST_CHECK_KEY, Date.now()); } async function checkBootstrapVersion() { try { log.info('检查引导脚本版本...'); const response = await httpGet(CONFIG.VERSION_SERVER.BOOTSTRAP_VERSION_URL, true, { 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' }); const versionMatch = response.match(/\/\/\s*@version\s+(\d+\.\d+\.\d+)/); if (versionMatch && versionMatch[1]) { const latestVersion = versionMatch[1]; const currentVersion = GM_info?.script?.version; if (!currentVersion) { log.warn('无法获取启动器版本,跳过启动器更新检查'); return false; } if (compareVersion(latestVersion, currentVersion) > 0) { log.warn(`引导脚本有新版本: v${currentVersion} → v${latestVersion}`); const showDialog = () => { const dialog = document.createElement('div'); dialog.id = 'bootstrap-update-dialog'; dialog.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #ffffff; border: 1px solid #e5e7eb; color: #111827; padding: 30px 40px; border-radius: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15); z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; text-align: center; max-width: 400px; `; dialog.innerHTML = `
⚠️
PP网课助手启动器需要更新
当前版本: v${currentVersion}
最新版本: v${latestVersion}
请先更新启动器,否则可能无法正常运行
安装完成后请刷新页面
立即更新 `; document.body.appendChild(dialog); log.warn('引导脚本需要更新,已停止执行'); }; if (document.body) { showDialog(); } else { document.addEventListener('DOMContentLoaded', showDialog); } return true; } else { log.success(`引导脚本已是最新版本 v${currentVersion}`); return false; } } } catch (e) { log.warn('检查引导脚本版本失败:', e.message); return false; } return false; } async function fetchLatestVersion() { const sources = [ { name: 'gh-proxy', url: CONFIG.SOURCE.GH_PROXY_URL, range: 'bytes=0-8191', timeout: 10000 }, { name: '脚本猫 meta', url: CONFIG.VERSION_SERVER.VERSION_URL, timeout: 30000 }, { name: 'CDN', url: CONFIG.SOURCE.CDN_URL, timeout: 20000 } ]; for (const source of sources) { try { log.info(`从${source.name}获取版本信息...`); let text; if (source.range && typeof fetch === 'function') { text = await fetchRangeText(source.url, source.range, source.timeout); } else { const url = source.url + (source.url.includes('?') ? '&' : '?') + 't=' + Date.now(); text = await httpGet(url, true, { 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' }, source.timeout); } const versionMatch = String(text || '').match(/\/\/\s*@version\s+(\d+\.\d+\.\d+)/); if (versionMatch && versionMatch[1]) { log.success(`${source.name}最新版本: ${versionMatch[1]}`); return { version: versionMatch[1], updateUrl: source.url }; } throw new Error('响应中没有 @version'); } catch (e) { log.warn(`${source.name}获取版本失败:`, e.message); } } throw new Error('所有版本源均失败'); } function isValidScriptCode(code) { return typeof code === 'string' && code.indexOf('// ==/UserScript==') !== -1 && /\/\/\s*@version\s+\d+\.\d+\.\d+/.test(code); } async function downloadScriptCode() { const headers = { 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' }; const sources = [ { name: 'gh-proxy', url: CONFIG.SOURCE.GH_PROXY_URL }, { name: '脚本猫', url: CONFIG.SCRIPTCAT.CODE_URL }, { name: 'CDN', url: CONFIG.SOURCE.CDN_URL } ]; let lastError = null; for (const source of sources) { try { log.info(`从${source.name}下载脚本代码...`); const url = source.url + (source.url.includes('?') ? '&' : '?') + 't=' + Date.now(); const code = await httpGet(url, true, headers, 30000); if (!isValidScriptCode(code)) { throw new Error('返回内容不是有效的用户脚本代码'); } const versionMatch = code.match(/\/\/\s*@version\s+(\d+\.\d+\.\d+)/); const version = versionMatch ? versionMatch[1] : 'unknown'; log.success(`${source.name}下载完成,版本: ${version}`); return { code, version, source: source.url }; } catch (e) { lastError = e; log.warn(`${source.name}下载失败:`, e.message); } } log.error('全部下载链接均失败:', lastError && lastError.message); throw lastError || new Error('脚本下载失败'); } function executeCode(code) { try { log.info('执行脚本代码...'); window.__CLOUD_SCRIPT_LOADED__ = true; const codeStart = code.indexOf('// ==/UserScript=='); let executableCode = code; if (codeStart !== -1) { executableCode = code.substring(codeStart + '// ==/UserScript=='.length); } executableCode = executableCode.trim(); if (typeof Vue === 'undefined') { throw new Error('Vue 未加载,请检查 @require 配置'); } log.info('依赖库检查: Vue=' + (typeof Vue !== 'undefined') + ', Pinia=' + (typeof Pinia !== 'undefined') + ', RxJS=' + (typeof rxjs !== 'undefined') + ', MD5=' + (typeof md5 !== 'undefined') + ', ElementPlus=' + (typeof ElementPlus !== 'undefined') + (typeof ElementPlus !== 'undefined' && ElementPlus.version ? '(' + ElementPlus.version + ')' : '')); eval(executableCode); log.success('脚本执行成功'); setupMainWindowObserver(); } catch (e) { log.error('脚本执行失败:', e.message); hideLoadingWindow(); throw e; } } function setupMainWindowObserver() { const closeAfterInit = () => { setTimeout(() => { log.info('脚本初始化完成,关闭加载窗口'); hideLoadingWindow(); }, 500); }; if (document.readyState === 'complete') { closeAfterInit(); } else { window.addEventListener('load', closeAfterInit); } } function showNotification(message, type = 'info') { const colors = { info: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', success: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', error: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)' }; const toast = document.createElement('div'); toast.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 12px 20px; background: ${colors[type]}; color: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 2147483647; font-size: 14px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; animation: slideIn 0.3s ease; `; toast.textContent = message; if (!document.getElementById('cloud-script-styles')) { const style = document.createElement('style'); style.id = 'cloud-script-styles'; style.textContent = ` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } `; document.head.appendChild(style); } document.body.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideIn 0.3s ease reverse'; setTimeout(() => toast.remove(), 300); }, 3000); } let cloudVersion = GM_getValue(CONFIG.VERSION_KEY, 'unknown'); window.__CLOUD_SCRIPT_VERSION__ = cloudVersion; async function main() { log.info('='.repeat(50)); log.info('云端热更新脚本启动(脚本猫版)'); log.info(`当前云端版本: v${cloudVersion}`); log.info('='.repeat(50)); if (window.self !== window.top) { log.info('检测到在 iframe 中运行,跳过'); return; } if (window.location.hostname === 'i.mooc.chaoxing.com') { log.info('检测到 i.mooc.chaoxing.com 首页,跳过加载窗口'); return; } const needsUpdate = await checkBootstrapVersion(); if (needsUpdate) { log.warn('引导脚本需要更新,停止后续执行'); return; } showLoadingWindow(); try { const localCache = getLocalCache(); const cacheValid = isValidScriptCode(localCache.code); if (!cacheValid && localCache.code) { log.warn('本地缓存的代码无效,将重新下载'); } let code = cacheValid ? localCache.code : ''; let version = cacheValid ? localCache.version : ''; cloudVersion = version || 'unknown'; window.__CLOUD_SCRIPT_VERSION__ = cloudVersion; try { log.info('检查版本更新...'); const latest = await fetchLatestVersion(); const latestVersion = latest.version; if (!code || !version || compareVersion(latestVersion, version) > 0) { log.info(`发现新版本: ${version || '无'} → ${latestVersion}`); const result = await downloadScriptCode(); code = result.code; version = result.version; cloudVersion = version; window.__CLOUD_SCRIPT_VERSION__ = version; saveLocalCache(version, code); if (document.body) { showNotification(`🔄 脚本已更新到 v${version}`, 'success'); } } else { log.success(`已是最新版本 v${version}`); } } catch (e) { log.warn('检查更新失败:', e.message); if (code) { log.info('使用本地缓存继续执行'); } else { log.info('无本地缓存,尝试直接从下载源获取脚本...'); const result = await downloadScriptCode(); code = result.code; version = result.version; cloudVersion = version; window.__CLOUD_SCRIPT_VERSION__ = version; saveLocalCache(version, code); } } if (code) { executeCode(code); } else { throw new Error('没有可执行的代码'); } } catch (e) { log.error('脚本启动失败:', e.message); hideLoadingWindow(); const showError = () => { const errorDiv = document.createElement('div'); errorDiv.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 24px 32px; background: white; border: 1px solid #fecaca; border-radius: 12px; color: #dc2626; z-index: 999999; font-size: 14px; text-align: center; box-shadow: 0 4px 12px rgba(0,0,0,0.1); `; errorDiv.innerHTML = `
❌
脚本加载失败
${e.message}
请检查网络连接或访问
脚本猫平台
`; document.body.appendChild(errorDiv); }; if (document.body) { showError(); } else { document.addEventListener('DOMContentLoaded', showError); } } } main(); })();