// ==UserScript== // @name Drcom 智能登录监听系统 // @namespace http://tampermonkey.net/ // @version 2.2 // @description 监听drcom登录状态,支持自动/手动登录 // @author Fromsko // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_notification // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_addValueChangeListener // @run-at document-start // ==/UserScript== ;(() => { 'use strict' // 配置 const CONFIG = { LOGIN_URL: 'drcom/login', // 登录接口特征 LOGOUT_URL: 'drcom/logout', // 登出接口特征 SUCCESS_TITLE: '上网登录页', // 登录成功后的标题 LOGIN_SUCCESS: '登录成功页', WELL_LOGIN_OUT: '注销页', CHECK_INTERVAL: 1000, // 标题检查间隔(毫秒) LOGIN_TIMEOUT: 5000, // 登录验证超时时间(毫秒) AUTO_LOGIN_DELAY: 1000, // 自动登录时间 (毫秒) } // 状态管理 const AuthState = { currentTitle: `${document.title}`, // 当前页面标题 pendingLogin: null, // 待验证的登录请求(存储 URL 和性能数据) lastLogoutTime: 0, // 上次登出时间 loginURL: '', autoLogin: GM_getValue('autoLogin', false), toggleAutoLogin() { this.autoLogin = !this.autoLogin GM_setValue('autoLogin', this.autoLogin) return this.autoLogin }, updateTitle(newTitle) { this.currentTitle = newTitle StorageManager.add({ type: 'title-change', title: newTitle, timestamp: Date.now(), }) // 如果标题变成 "注销页",并且有待验证的登录请求,则触发登录成功 if (this.pendingLogin && newTitle === CONFIG.LOGIN_SUCCESS) { triggerEvent('login-success', { url: this.pendingLogin.url, // 登录请求的完整 URL detail: this.pendingLogin.entry, // 性能数据 }) GM_setValue('login_url', this.pendingLogin.url) this.pendingLogin = null // 清除待验证状态 } }, } // 网络请求模块 const Network = { async login() { const url = GM_getValue('login_url') console.log('logout', url) return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: url, onload: (xhr) => { AuthState.loginURL = url resolve(xhr.responseText) }, }) }) }, async logout() { const url = GM_getValue('logout_url') console.log('logout', url) return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'GET', url: url, onload: resolve, }) }) }, } const StorageManager = { STORAGE_KEY: 'drcom_auth_logs', getAll() { return GM_getValue(this.STORAGE_KEY, []) }, add(data) { const records = this.getAll() records.push({ id: Date.now(), ...data, }) GM_setValue(this.STORAGE_KEY, records) }, getItem(key) { const records = this.getAll() const item = records.find((record) => record.type === key) return item || null }, } // 浏览器控制台颜色日志 const useWebColorLogger = () => { const isProduction = false const isEmpty = (value) => { return value == null || value === undefined || value === '' } const prettyPrint = (title, text, color) => { if (isProduction) return console.log( `%c ${title} %c ${text} %c`, `background:${color};border:1px solid ${color}; padding: 1px; border-radius: 2px 0 0 2px; color: #fff;`, `border:1px solid ${color}; padding: 1px; border-radius: 0 2px 2px 0; color: ${color};`, 'background:transparent' ) } const info = (textOrTitle, content = '') => { const title = isEmpty(content) ? 'Info' : textOrTitle const text = isEmpty(content) ? textOrTitle : content prettyPrint(title, text, '#909399') } const error = (textOrTitle, content = '') => { const title = isEmpty(content) ? 'Error' : textOrTitle const text = isEmpty(content) ? textOrTitle : content prettyPrint(title, text, '#F56C6C') } const warning = (textOrTitle, content = '') => { const title = isEmpty(content) ? 'Warning' : textOrTitle const text = isEmpty(content) ? textOrTitle : content prettyPrint(title, text, '#E6A23C') } const success = (textOrTitle, content = '') => { const title = isEmpty(content) ? 'Success ' : textOrTitle const text = isEmpty(content) ? textOrTitle : content prettyPrint(title, text, '#67C23A') } const table = () => { const data = [ { id: 1, name: 'Alice', age: 25 }, { id: 2, name: 'Bob', age: 30 }, { id: 3, name: 'Charlie', age: 35 }, ] console.log( '%c id%c name%c age', 'color: white; background-color: black; padding: 2px 10px;', 'color: white; background-color: black; padding: 2px 10px;', 'color: white; background-color: black; padding: 2px 10px;' ) data.forEach((row) => { console.log( `%c ${row.id} %c ${row.name} %c ${row.age} `, 'color: black; background-color: lightgray; padding: 2px 10px;', 'color: black; background-color: lightgray; padding: 2px 10px;', 'color: black; background-color: lightgray; padding: 2px 10px;' ) }) } const picture = (url, scale = 1) => { if (isProduction) return const img = new Image() img.crossOrigin = 'anonymous' img.onload = () => { const c = document.createElement('canvas') const ctx = c.getContext('2d') if (ctx) { c.width = img.width c.height = img.height ctx.fillStyle = 'red' ctx.fillRect(0, 0, c.width, c.height) ctx.drawImage(img, 0, 0) const dataUri = c.toDataURL('image/png') console.log( `%c sup?`, `font-size: 1px; padding: ${Math.floor((img.height * scale) / 2)}px ${Math.floor((img.width * scale) / 2)}px; background-image: url(${dataUri}); background-repeat: no-repeat; background-size: ${img.width * scale}px ${img.height * scale}px; color: transparent; ` ) } } img.src = url } return { info, error, warning, success, picture, table, } } // 监听资源加载(仅监控 Performance API,不 Hook fetch) const setupResourceObserver = () => { new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { // 检测登出请求 if (entry.name.includes(CONFIG.LOGOUT_URL)) { triggerEvent('logout', { url: entry.name, detail: entry, }) } // 检测登录请求 else if (entry.name.includes(CONFIG.LOGIN_URL)) { AuthState.pendingLogin = { url: entry.name, entry: entry, timestamp: Date.now(), } triggerEvent('login-attempt', { url: entry.name, detail: entry, }) // 设置超时清除(避免内存泄漏) setTimeout(() => { if (AuthState.pendingLogin?.timestamp === Date.now()) { console.log('⌛ 登录验证超时:', entry.name) AuthState.pendingLogin = null } }, CONFIG.LOGIN_TIMEOUT) } }) }).observe({ type: 'resource', buffered: true }) } // 监听标题变化(轮询方式,避免 MutationObserver 兼容性问题) const setupTitleWatcher = () => { const checkTitle = () => { const currentTitle = document.title if (currentTitle !== AuthState.currentTitle) { AuthState.updateTitle(currentTitle) } setTimeout(checkTitle, CONFIG.CHECK_INTERVAL) } checkTitle() } // 触发事件(控制台输出 + 存储) const triggerEvent = (type, data) => { const eventData = { type, timestamp: Date.now(), ...data, } switch (type) { case 'logout': AuthState.lastLogoutTime = Date.now() GM_setValue('logout_url', data.url) console.warn('🔴 检测到登出:', data.url) GM_notification({ title: '用户登出', text: `URL: ${data.url}`, timeout: 3000, }) break case 'login-attempt': console.log('🟡 检测到登录尝试:', data.initiatorType) break case 'login-success': console.log( '🟢 登录成功!\n', '请求 URL:', data.url, '\n', '验证标题:', `"${AuthState.currentTitle}"`, '\n', '性能数据:', data.detail ) GM_notification({ title: '登录成功', text: `URL: ${data.url}`, timeout: 5000, }) break } StorageManager.add(eventData) } // 初始化 setupResourceObserver() setupTitleWatcher() // 注册菜单命令 GM_registerMenuCommand('切换自动登录', () => { const state = AuthState.toggleAutoLogin() GM_notification({ title: '自动登录已' + (state ? '开启' : '关闭'), timeout: 2000, }) }) const handleEvent = (view, callback = () => {}) => { let removeHtmlStyle = () => { let htmlElement = document.documentElement htmlElement.removeAttribute('style') document.body.style.margin = '0' document.body.style.display = 'flex' document.body.style.alignItems = 'center' document.body.style.justifyContent = 'center' document.body.style.backgroundColor = 'skyblue' } let timer = setTimeout(function () { document.body.innerHTML = view callback() removeHtmlStyle() clearTimeout(timer) }, 500) } // 界面渲染器 const UIRenderer = { reload() { let timer = setTimeout(function () { window.location.reload() clearTimeout(timer) }, 500) }, showLoginForm() { handleEvent( `