// ==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));
}
})();