// ==UserScript== // @name 米饭签到助手(终极稳定版) // @namespace https://mifan.61.com/ // @version 2.6 // @description 自动签到,右上角单浮窗,右下角可视化面板配置企微推送,设置按钮独立优先加载 // @author You // @match https://mifan.61.com/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @connect qyapi.weixin.qq.com // @run-at document-start // @license Gemini // ==/UserScript== (function() { 'use strict'; // 注入全局 CSS 样式 GM_addStyle(` /* 右上角签到浮窗 */ .auto-sign-float { position: fixed; top: 20px; right: 20px; z-index: 99999; background: rgba(33,150,243,0.9); color: #fff; padding: 12px 20px; border-radius: 8px; font-size: 16px; font-family: "Microsoft YaHei", sans-serif; box-shadow: 0 4px 12px rgba(0,0,0,0.3); transition: opacity 0.3s, background 0.3s; opacity: 1; pointer-events: none; } .auto-sign-float.success { background: rgba(76,175,80,0.9); } .auto-sign-float.fail { background: rgba(244,67,54,0.9); } .auto-sign-float.warn { background: rgba(255,152,0,0.9); } /* 右下角设置按钮 */ .sign-config-btn { position: fixed; bottom: 25px; right: 25px; z-index: 99998; width: 42px; height: 42px; background: #2196F3; color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 20px; cursor: pointer; box-shadow: 0 4px 10px rgba(0,0,0,0.3); transition: transform 0.2s, background 0.2s; user-select: none; } .sign-config-btn:hover { transform: scale(1.1); background: #1976D2; } /* 配置遮罩与面板 */ .sign-config-modal { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.4); z-index: 999999; display: flex; align-items: center; justify-content: center; font-family: "Microsoft YaHei", sans-serif; } .sign-config-panel { background: #fff; width: 360px; padding: 20px 24px; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.2); box-sizing: border-box; color: #333; } .sign-config-panel h3 { margin: 0 0 16px 0; font-size: 18px; color: #2196F3; text-align: center; } .sign-config-group { margin-bottom: 12px; } .sign-config-group label { display: block; font-size: 13px; color: #666; margin-bottom: 4px; } .sign-config-group input { width: 100%; padding: 8px 10px; border: 1px solid #ccc; border-radius: 6px; font-size: 14px; box-sizing: border-box; outline: none; transition: border-color 0.2s; } .sign-config-group input:focus { border-color: #2196F3; } .sign-config-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; } .sign-config-actions button { padding: 8px 16px; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; transition: background 0.2s; } .btn-cancel { background: #e0e0e0; color: #333; } .btn-cancel:hover { background: #d6d6d6; } .btn-save { background: #2196F3; color: #fff; } .btn-save:hover { background: #1976D2; } `); // ==================== 配置存储管理 ==================== function getConfig() { return { accountName: localStorage.getItem('mifan_account_name') || '', mimiAccount: localStorage.getItem('mifan_mimi_account') || '', wechatWebhook: localStorage.getItem('mifan_wechat_webhook') || '' }; } function saveConfig(accountName, mimiAccount, wechatWebhook) { localStorage.setItem('mifan_account_name', accountName.trim()); localStorage.setItem('mifan_mimi_account', mimiAccount.trim()); localStorage.setItem('mifan_wechat_webhook', wechatWebhook.trim()); } // ==================== 创建右下角配置 UI ==================== function createConfigUI() { if (document.getElementById('mifan-config-btn')) return; const btn = document.createElement('div'); btn.id = 'mifan-config-btn'; btn.className = 'sign-config-btn'; btn.innerHTML = '⚙️'; btn.title = '设置企业微信推送'; btn.onclick = openConfigModal; if (document.body) { document.body.appendChild(btn); } else { window.addEventListener('DOMContentLoaded', () => document.body.appendChild(btn)); } } function openConfigModal() { if (document.getElementById('mifan-config-modal')) return; const config = getConfig(); const modal = document.createElement('div'); modal.id = 'mifan-config-modal'; modal.className = 'sign-config-modal'; modal.innerHTML = `

推送机器人配置

`; document.body.appendChild(modal); document.getElementById('cfg-btn-cancel').onclick = () => modal.remove(); document.getElementById('cfg-btn-save').onclick = () => { const accName = document.getElementById('cfg-account-name').value; const mimiAcc = document.getElementById('cfg-mimi-account').value; const webhook = document.getElementById('cfg-wechat-webhook').value; saveConfig(accName, mimiAcc, webhook); alert('配置已成功保存!'); modal.remove(); }; } // ==================== 浮窗逻辑 ==================== let floatEl = null; function updateFloatMsg(text, type = 'info') { document.querySelectorAll('.auto-sign-float').forEach(el => { if (el !== floatEl) el.remove(); }); if (!floatEl || !document.body.contains(floatEl)) { floatEl = document.createElement('div'); floatEl.className = 'auto-sign-float'; document.body.appendChild(floatEl); } floatEl.textContent = text; floatEl.className = `auto-sign-float ${type}`; } function removeFloat(delay = 3000) { setTimeout(() => { if (floatEl) { floatEl.style.opacity = '0'; setTimeout(() => { if (floatEl) floatEl.remove(); floatEl = null; }, 300); } }, delay); } // ==================== 推送逻辑 ==================== function sendWechatNotification(statusText) { const config = getConfig(); if (!config.wechatWebhook) { console.log("未配置企业微信 Webhook 地址,跳过推送。"); return; } const now = new Date(); const timeStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`; const markdownContent = `### 网页自动签到通知\n` + `> **账号名称**:${config.accountName || '未填写'}\n` + `> **米米账号**:${config.mimiAccount || '未填写'}\n` + `> **签到状态**:${statusText}\n` + `> **签到时间**:${timeStr}`; GM_xmlhttpRequest({ method: "POST", url: config.wechatWebhook, headers: { "Content-Type": "application/json" }, data: JSON.stringify({ msgtype: "markdown", markdown: { content: markdownContent } }), onload: function(response) { console.log("企业微信推送响应:", response.responseText); }, onerror: function(err) { console.error("企业微信推送失败:", err); } }); } // ==================== 工具函数 ==================== function waitForElement(xpath, timeout = 10000) { return new Promise((resolve, reject) => { const startTime = Date.now(); const check = () => { const el = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; if (el) { resolve(el); } else if (timeout > 0 && Date.now() - startTime > timeout) { reject(new Error(`等待元素超时: ${xpath}`)); } else { setTimeout(check, 500); } }; check(); }); } function clickElement(el) { if (!el) return false; el.click(); ['mousedown', 'mouseup', 'click'].forEach(type => { el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true })); }); return true; } function isSignSuccess() { const successKeywords = ['签到成功', '已签到', '今日已签', '签到完成']; const signBtn = document.evaluate('//*[contains(text(),"签到")]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; if (signBtn && signBtn.textContent.includes('已签到')) return true; const bodyText = document.body.innerText; return successKeywords.some(kw => bodyText.includes(kw)); } // ==================== 主逻辑 ==================== async function executeSign(myBtn) { try { updateFloatMsg('签到中...', 'info'); clickElement(myBtn); const signBtn = await waitForElement('//*[contains(text(),"签到") and (self::a or self::button or self::span or self::div)]', 15000); let success = false; let retryCount = 0; const maxRetries = 20; while (!success && retryCount < maxRetries) { if (retryCount > 0) { await new Promise(r => setTimeout(r, 3000)); } const currentSignBtn = document.evaluate( '//*[contains(text(),"签到") and (self::a or self::button or self::span or self::div)]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ).singleNodeValue; if (!currentSignBtn) { if (isSignSuccess()) { success = true; break; } retryCount++; continue; } clickElement(currentSignBtn); await new Promise(r => setTimeout(r, 2000)); if (isSignSuccess()) { success = true; } else { retryCount++; } } if (success) { updateFloatMsg('签到成功', 'success'); sendWechatNotification('签到成功'); removeFloat(4000); } else { throw new Error('重试达上限'); } } catch (e) { console.error('米饭签到失败:', e); updateFloatMsg('签到失败', 'fail'); removeFloat(5000); } } async function startAutoSign() { const xpathMy = '//a[contains(text(),"我的")] | //span[contains(text(),"我的")] | //div[contains(text(),"我的")]'; let myBtn = null; try { myBtn = await waitForElement(xpathMy, 3000); } catch (err) { updateFloatMsg('未登录', 'warn'); while (!myBtn) { await new Promise(r => setTimeout(r, 1000)); const el = document.evaluate(xpathMy, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; if (el) { myBtn = el; } } } if (myBtn) { executeSign(myBtn); } } // 1. 立即独立创建右下角设置按钮,绝不被任何登录等待逻辑阻塞 createConfigUI(); // 2. 异步执行签到和未登录检测逻辑 if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(startAutoSign, 1000); } else { window.addEventListener('DOMContentLoaded', () => setTimeout(startAutoSign, 1000)); } })();