// ==UserScript== // @name 苦力怕论坛自动登录签到 // @namespace https://bbs.tampermonkey.net.cn/ // @version 0.9.7 // @description 自动登录苦力怕论坛并执行签到操作 // @author ShixmSimon // @crontab * * once * * // @icon https://klpbbs.com/favicon.ico // @grant GM_notification // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_log // @connect klpbbs.com // @connect * // @match https://klpbbs.com/* // ==/UserScript== /* ==UserConfig== Config: klp_username: title: 苦力怕论坛用户名 type: text default: "" klp_password: title: 苦力怕论坛密码 type: text password: true enable_api_push: title: 启用API推送 type: checkbox default: false meow_user_id: title: API用户ID description: 在API中使用的用户昵称/ID type: text default: "" meow_base_url: title: API基础地址 description: 默认为 https://api.chuckfang.com/ type: text default: "https://api.chuckfang.com/" ==/UserConfig== */ const MAX_RETRIES = 3; const RETRY_DELAY = 2000; // API推送函数 function pushToAPI(title, content, imgUrl = "https://klpbbs.com/favicon.ico") { return new Promise((resolve) => { const enableApiPush = GM_getValue("Config.enable_api_push", false); const apiUserId = GM_getValue("Config.meow_user_id", ""); const apiBaseUrl = GM_getValue("Config.meow_base_url", "https://api.chuckfang.com/"); if (!enableApiPush || !apiUserId) { resolve(false); return; } // 清理API基础地址,确保没有多余的斜杠 let cleanApiBaseUrl = apiBaseUrl.trim(); if (!cleanApiBaseUrl.endsWith('/')) { cleanApiBaseUrl += '/'; } // 构建API URL const apiUrl = `${cleanApiBaseUrl}${encodeURIComponent(apiUserId)}`; // 准备请求数据 const requestData = { title: title, msg: content, imgUrl: imgUrl }; GM_xmlhttpRequest({ method: 'POST', url: apiUrl, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, data: JSON.stringify(requestData), timeout: 10000, onload: function(response) { try { const result = JSON.parse(response.responseText); if (response.status >= 200 && response.status < 300 && result.status === 200) { GM_log(`API推送成功: ${title}`); resolve(true); } else { GM_log(`API推送失败,状态码: ${response.status}, 响应: ${response.responseText}`); resolve(false); } } catch (error) { GM_log(`API响应解析错误: ${error}`); resolve(false); } }, onerror: function(error) { GM_log(`API推送错误: ${error}`); resolve(false); }, ontimeout: function() { GM_log(`API推送超时: ${apiUrl}`); resolve(false); } }); }); } async function showNotificationWithPush(type, title, details = null) { let notificationText = ''; if (details) { for (const [key, value] of Object.entries(details)) { notificationText += `${key}: ${value}\n`; } notificationText = notificationText.trim(); } // 显示本地通知 GM_notification({ title: `${type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️'} ${title}`, text: notificationText, timeout: type === 'error' ? 0 : 0, highlight: type === 'error' }); // 发送API推送 await pushToAPI(title, notificationText || title, "https://s41.ax1x.com/2026/01/08/pZwdp79.png"); } // 检测论坛是否维护 function checkMaintenancePage(html) { // 检测维护页面的多种特征 const maintenanceIndicators = [ /维护公告.*苦力怕论坛/i, /苦力怕论坛临时关闭维护/i, /维护升级进行中/i, /状态:.*临时关闭维护/i, /class="badge".*维护升级进行中/i, /class="pill".*临时关闭维护/i, /论坛将进行一段时间的长期维护升级/i, /系统维护升级工作/i ]; return maintenanceIndicators.some(indicator => indicator.test(html)); } // 解析维护页面信息 function parseMaintenanceInfo(html) { try { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); let title = '论坛维护中'; let startTime = '未知'; let endTime = '未知'; let reason = '系统升级与性能优化'; let contactInfo = []; // 尝试解析标题 const titleElement = doc.querySelector('h1, .title h1'); if (titleElement) { const titleText = titleElement.textContent.trim(); if (titleText && titleText !== '') { title = titleText; } } // 尝试解析时间信息 const timeRegex = /(\d{4}年\d{1,2}月\d{1,2}日)/g; const times = html.match(timeRegex); if (times && times.length > 0) { startTime = times[0]; if (times.length > 1) { endTime = times[1]; } } // 尝试解析原因 const reasonMatches = html.match(/为(.*?),论坛/g) || html.match(/由于(.*?),/g) || html.match(/原因[::](.*?)[。\n]/g); if (reasonMatches && reasonMatches[0]) { const extractedReason = reasonMatches[0] .replace(/为/, '') .replace(/由于/, '') .replace(/原因[::]/, '') .replace(/,论坛/, '') .replace(/,/, '') .trim(); if (extractedReason && extractedReason !== '') { reason = extractedReason; } } // 尝试解析联系信息 const emailElements = doc.querySelectorAll('a[href^="mailto:"]'); emailElements.forEach(el => { const email = el.textContent.trim(); if (email && email.includes('@')) { contactInfo.push(email); } }); // 如果没有找到邮箱,尝试从文本中提取 if (contactInfo.length === 0) { const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g; const emails = html.match(emailRegex); if (emails) { contactInfo = emails; } } return { isMaintenance: true, title: title, startTime: startTime, endTime: endTime, reason: reason, contactInfo: contactInfo.slice(0, 3) // 最多取3个联系方式 }; } catch (error) { return { isMaintenance: true, title: '论坛维护中', startTime: '未知', endTime: '未知', reason: '系统升级与维护', contactInfo: [] }; } } async function retryWrapper(fn, retries = MAX_RETRIES, delay = RETRY_DELAY) { for (let i = 0; i < retries; i++) { try { return await fn(); } catch (error) { if (i === retries - 1) throw error; await new Promise(resolve => setTimeout(resolve, delay)); } } } function fetchAPI(url, method, data = null, headers = {}) { return new Promise((resolve, reject) => { const defaultHeaders = { 'Referer': `https://klpbbs.com/`, 'User-Agent': navigator.userAgent }; if (method.toUpperCase() === 'POST' && data) { defaultHeaders['Content-Type'] = 'application/x-www-form-urlencoded'; } const finalHeaders = {...defaultHeaders, ...headers}; GM_xmlhttpRequest({ method: method, url: url, data: data, headers: finalHeaders, onload: (res) => { if (res.status >= 200 && res.status < 300) { // 检查是否为维护页面 if (checkMaintenancePage(res.responseText)) { const maintenanceInfo = parseMaintenanceInfo(res.responseText); reject({ type: 'maintenance', message: '论坛正在维护中', info: maintenanceInfo, html: res.responseText }); } else { resolve({ text: res.responseText, finalUrl: res.finalUrl, status: res.status }); } } else { reject(new Error(`请求失败:${res.status}`)); } }, onerror: (err) => reject(err) }); }); } function getFormhash(html) { const formhashSources = [ /name="formhash" value="([a-f0-9]+)"/, /formhash=([a-f0-9]+)/, /k_misign-sign\.html\?formhash=([a-f0-9]+)/ ]; for (const regex of formhashSources) { const match = html.match(regex); if (match) { return match[1]; } } try { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const formhashInput = doc.querySelector('input[name="formhash"]'); if (formhashInput) { return formhashInput.value; } } catch (e) { // 忽略解析错误 } return null; } async function detectVersion() { try { const indexUrl = `https://klpbbs.com/`; const response = await fetchAPI(indexUrl, 'GET'); const isMobile = response.text.includes('comiis_app_block') || response.text.includes('comiis_mh_') || response.text.includes('mobile=2'); const isDesktop = response.text.includes('id="toptb"') || response.text.includes('id="fjump_menu"') || response.text.includes('class="wp"'); if (isMobile) { return { type: 'mobile', loginUrl: `https://klpbbs.com/member.php?mod=logging&action=login&mobile=2`, signUrl: `https://klpbbs.com/k_misign-sign.html?mobile=2`, creditUrl: `https://klpbbs.com/home.php?mod=spacecp&ac=credit&op=base&mobile=2`, creditLogUrl: `https://klpbbs.com/home.php?mod=spacecp&ac=credit&op=log&mobile=2`, profileUrl: `https://klpbbs.com/home.php?mod=space&do=profile&mobile=2` }; } else if (isDesktop) { return { type: 'desktop', loginUrl: `https://klpbbs.com/member.php?mod=logging&action=login`, signUrl: `https://klpbbs.com/k_misign-sign.html`, creditUrl: `https://klpbbs.com/home.php?mod=spacecp&ac=credit&op=base`, creditLogUrl: `https://klpbbs.com/home.php?mod=spacecp&ac=credit&op=log`, profileUrl: `https://klpbbs.com/home.php?mod=space&do=profile` }; } return { type: 'desktop', loginUrl: `https://klpbbs.com/member.php?mod=logging&action=login`, signUrl: `https://klpbbs.com/k_misign-sign.html`, creditUrl: `https://klpbbs.com/home.php?mod=spacecp&ac=credit&op=base`, creditLogUrl: `https://klpbbs.com/home.php?mod=spacecp&ac=credit&op=log`, profileUrl: `https://klpbbs.com/home.php?mod=space&do=profile` }; } catch (error) { if (error.type === 'maintenance') { throw error; } return { type: 'desktop', loginUrl: `https://klpbbs.com/member.php?mod=logging&action=login`, signUrl: `https://klpbbs.com/k_misign-sign.html`, creditUrl: `https://klpbbs.com/home.php?mod=spacecp&ac=credit&op=base`, creditLogUrl: `https://klpbbs.com/home.php?mod=spacecp&ac=credit&op=log`, profileUrl: `https://klpbbs.com/home.php?mod=space&do=profile` }; } } async function checkLoginStatus(versionInfo) { try { const { profileUrl } = versionInfo; const profileRes = await fetchAPI(profileUrl, 'GET'); const isLoggedIn = profileRes.text.includes('退出') || profileRes.text.includes('我的中心') || profileRes.text.includes('个人中心') || profileRes.text.includes('修改头像') || profileRes.text.includes('个人资料'); return isLoggedIn; } catch (error) { if (error.type === 'maintenance') { throw error; } return false; } } async function checkSignStatus(versionInfo) { try { const { signUrl } = versionInfo; const signPage = await fetchAPI(signUrl, 'GET'); const html = signPage.text; // 针对手机版的专门检测 if (versionInfo.type === 'mobile') { // 手机版未签到:有可点击的签到按钮 const notSignedMobile = (html.includes('id="signresult"') && html.includes('onclick="ajaxsign();"') && html.includes('>签到<')) || (html.includes('class="comiis_btn bg_c f_f"') && html.includes('>签到<')); // 手机版已签到:按钮显示"已签到"且没有onclick事件 const signedMobile = html.includes('>已签到<') && (html.includes('class="comiis_btn bg_b f_c"') || !html.includes('onclick="ajaxsign();"')); if (notSignedMobile) { return false; } else if (signedMobile) { return true; } } // 使用DOM解析进行更精确的检测 const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // 方法1: 检查签到按钮状态 const signButton = doc.querySelector('input[type="submit"][value*="签到"], button[value*="签到"], a[href*="sign"][onclick*="签到"], .sign-btn, #sign-btn, #signresult, .comiis_btn'); // 方法2: 检查已签到提示 const signedText = doc.querySelector('.signed-text, .sign-success, .qiandao-success, .comiis_btn.bg_b'); // 方法3: 检查按钮文本内容 let buttonText = ''; if (signButton) { buttonText = signButton.textContent || signButton.value || ''; } // 逻辑判断优先级:明确迹象 > DOM元素 > 文本匹配 if (signButton && !signButton.disabled && (buttonText.includes('签到') && !buttonText.includes('已签到'))) { return false; } if (signedText || (signButton && buttonText.includes('已签到'))) { return true; } // 文本匹配(作为备用方案) const notSignedIndicators = [ /您今天还没有签到/, /立即签到/, /class="signbtn"/, /id="signbtn"/, /qiandao.*?button/i ]; const signedIndicators = [ /您的签到排名/, /今日已签/, /签到成功/, /已签到/, /class="signed"/, /id="signed"/, /今天已经签到/ ]; let notSignedCount = notSignedIndicators.filter(indicator => indicator.test(html)).length; let signedCount = signedIndicators.filter(indicator => indicator.test(html)).length; if (notSignedCount > signedCount) { return false; } else if (signedCount > notSignedCount) { return true; } // 如果无法确定状态,保守地返回未签到 GM_setValue('last_sign_page', html); return false; } catch (error) { if (error.type === 'maintenance') { throw error; } // 出错时返回未签到 return false; } } async function klpLogin(username, password, versionInfo) { try { const { loginUrl, type } = versionInfo; let formhash, loginhash, loginParams; const loginPage = await fetchAPI(loginUrl, 'GET'); formhash = getFormhash(loginPage.text); if (!formhash) { throw new Error('获取登录令牌失败'); } loginParams = new URLSearchParams(); loginParams.append('formhash', formhash); loginParams.append('username', username); loginParams.append('password', password); if (type === 'mobile') { loginhash = loginPage.text.match(/loginhash=([a-zA-Z0-9]{4,})/)?.[1] || ''; loginParams.append('referer', `https://klpbbs.com/?mobile=2`); loginParams.append('fastloginfield', 'username'); loginParams.append('cookietime', '31104000'); loginParams.append('questionid', '0'); loginParams.append('answer', ''); loginParams.append('submit', '登录'); } else { loginParams.append('loginsubmit', 'true'); loginParams.append('handlekey', 'login'); loginParams.append('referer', `https://klpbbs.com/`); } let loginPostUrl; if (type === 'mobile') { loginPostUrl = `https://klpbbs.com/member.php?mod=logging&action=login&loginsubmit=yes&loginhash=${loginhash}&inajax=1`; } else { loginPostUrl = `https://klpbbs.com/member.php?mod=logging&action=login&loginsubmit=yes&infloat=yes&handlekey=login&inajax=1`; } const loginRes = await fetchAPI( loginPostUrl, 'POST', loginParams.toString() ); const isLoggedIn = await checkLoginStatus(versionInfo); if (!isLoggedIn) { if (loginRes.text.includes('密码错误')) throw new Error('登录失败:密码错误'); if (loginRes.text.includes('登录受限')) throw new Error('登录失败:尝试次数过多'); if (loginRes.text.includes('安全提问')) throw new Error('登录失败:需要安全提问'); throw new Error('登录失败:未知错误'); } return true; } catch (error) { if (error.type === 'maintenance') { throw error; } throw new Error(`苦力怕论坛登录失败: ${error.message}`); } } async function klpSignIn(versionInfo) { try { const { signUrl, type } = versionInfo; const isAlreadySigned = await checkSignStatus(versionInfo); if (isAlreadySigned) { return 'already'; } const signPage = await fetchAPI(signUrl, 'GET'); let formhash = getFormhash(signPage.text); if (!formhash) { const formhashMatch = signPage.text.match(/k_misign-sign\.html\?formhash=([a-f0-9]+)/); if (formhashMatch) { formhash = formhashMatch[1]; } else { throw new Error('获取签到令牌失败'); } } const signActionUrl = `https://klpbbs.com/plugin.php?id=k_misign:sign&operation=qiandao&formhash=${formhash}&format=empty&inajax=1`; const signRes = await fetchAPI(signActionUrl, 'GET', null); // 等待1秒让服务器处理 await new Promise(resolve => setTimeout(resolve, 1000)); const finalStatus = await checkSignStatus(versionInfo); if (finalStatus) { return 'success'; } else if (signRes.text.includes('签到成功') || signRes.text.includes('您的签到排名') || signRes.text.includes('window.location.reload')) { return 'success'; } else if (signRes.text.includes('今天已经签到')) { return 'already'; } else { return 'fail'; } } catch (error) { if (error.type === 'maintenance') { throw error; } throw new Error(`苦力怕论坛签到失败: ${error.message}`); } } async function klpGetCreditInfo(versionInfo) { try { const { creditUrl, creditLogUrl, type } = versionInfo; const creditHtml = await fetchAPI(creditUrl, 'GET'); const creditAmount = parseCreditAmount(creditHtml.text, type); const creditLogHtml = await fetchAPI(creditLogUrl, 'GET'); const signInfo = parseCreditLogPage(creditLogHtml.text, type); return { creditAmount: creditAmount, lastSignTime: signInfo.lastSignTime, lastSignReward: signInfo.lastSignReward }; } catch (error) { if (error.type === 'maintenance') { throw error; } throw new Error(`获取积分信息失败: ${error.message}`); } } function parseCreditLogPage(html, type) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); let lastSignTime = '无记录'; let lastSignReward = '无记录'; if (type === 'desktop') { const rows = doc.querySelectorAll('table tbody tr'); for (let i = 1; i < rows.length; i++) { const cells = rows[i].querySelectorAll('td'); if (cells.length >= 4) { const operation = cells[0].textContent.trim(); if (operation === '每日签到') { const rewardSpan = cells[1].querySelector('.xi1'); if (rewardSpan) { lastSignReward = rewardSpan.textContent.trim(); } lastSignTime = cells[3].textContent.trim(); break; } } } } else { const signRows = doc.querySelectorAll('tr'); for (const row of signRows) { const cells = row.querySelectorAll('td'); if (cells.length >= 4) { const operation = cells[0].textContent.trim(); if (operation === '每日签到') { const rewardText = cells[1].textContent.trim(); const rewardMatch = rewardText.match(/铁粒\s*([+-]\d+)/); if (rewardMatch) { lastSignReward = rewardMatch[1]; } else { const rewardSpan = cells[1].querySelector('.xi1'); if (rewardSpan) { lastSignReward = rewardSpan.textContent.trim(); } } lastSignTime = cells[3].textContent.trim(); break; } } } if (lastSignTime === '无记录') { const mobileItems = doc.querySelectorAll('li.b_b'); for (const item of mobileItems) { const operationEl = item.querySelector('span.f_d:not(.y)'); const timeEl = item.querySelector('span.y'); const creditEl = item.querySelector('span.xi1'); if (operationEl && timeEl && creditEl && (operationEl.textContent.includes('打卡签到') || operationEl.textContent.includes('每日签到'))) { lastSignTime = timeEl.textContent.trim(); lastSignReward = creditEl.textContent.trim(); break; } } } } return { lastSignTime: lastSignTime, lastSignReward: lastSignReward }; } function parseCreditAmount(html, type) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); let creditAmount = '未知'; if (type === 'desktop') { const creditLi = Array.from(doc.querySelectorAll('ul.creditl li')) .find(li => li.textContent.includes('铁粒')); if (creditLi) { const match = creditLi.textContent.match(/铁粒:\s*(\d+)/); if (match) creditAmount = match[1]; } } else { const creditText = Array.from(doc.querySelectorAll('*')) .map(el => el.textContent) .find(text => text.includes('铁粒') && /\d+/.test(text)); if (creditText) { const match = creditText.match(/铁粒\s*[::]?\s*(\d+)/); if (match) creditAmount = match[1]; } } return creditAmount; } // 主函数 (async function() { 'use strict'; const config = { klp_username: GM_getValue("Config.klp_username", ''), klp_password: GM_getValue("Config.klp_password", ''), enable_api_push: GM_getValue("Config.enable_api_push", false), api_user_id: GM_getValue("Config.meow_user_id", ""), api_base_url: GM_getValue("Config.meow_base_url", "https://api.chuckfang.com/") }; const results = { klp: { login: false, sign: false, creditInfo: null, version: 'unknown', isAlreadyLoggedIn: false, isAlreadySigned: false, errors: [] } }; let notificationSent = false; if (config.klp_username && config.klp_password) { try { const versionInfo = await retryWrapper(() => detectVersion()); results.klp.version = versionInfo.type; const isLoggedIn = await checkLoginStatus(versionInfo); results.klp.isAlreadyLoggedIn = isLoggedIn; if (isLoggedIn) { results.klp.login = true; } else { await retryWrapper(() => klpLogin( config.klp_username, config.klp_password, versionInfo )); results.klp.login = true; } const isSigned = await checkSignStatus(versionInfo); results.klp.isAlreadySigned = isSigned; if (isSigned) { results.klp.sign = 'already'; } else { const signResult = await retryWrapper(() => klpSignIn(versionInfo)); results.klp.sign = signResult; // 签到后再次检查状态以确保准确性 if (signResult === 'success') { const finalCheck = await checkSignStatus(versionInfo); if (!finalCheck) { results.klp.sign = 'uncertain'; } } } const creditInfo = await retryWrapper(() => klpGetCreditInfo(versionInfo)); results.klp.creditInfo = creditInfo; } catch (error) { // 特殊处理维护错误 if (error.type === 'maintenance') { const maintenanceInfo = error.info; const details = { '状态': '论坛正在维护中', '开始时间': maintenanceInfo.startTime, '预计结束': maintenanceInfo.endTime, '维护原因': maintenanceInfo.reason }; if (maintenanceInfo.contactInfo && maintenanceInfo.contactInfo.length > 0) { details['联系方式'] = maintenanceInfo.contactInfo.join(', '); } if (!notificationSent) { await showNotificationWithPush('error', maintenanceInfo.title, details); notificationSent = true; } return; // 维护期间不执行后续操作 } results.klp.errors.push(error.message); } } else { results.klp.errors.push('苦力怕论坛账号密码配置不完整'); } const klp = results.klp; if (klp.errors.length > 0) { if (!notificationSent) { await showNotificationWithPush('error', '苦力怕论坛签到错误', {错误信息: klp.errors.join('; ')}); notificationSent = true; } } else if (!notificationSent) { let statusTitle = '苦力怕论坛签到'; let statusType = 'info'; let details = {}; if (klp.creditInfo) { if (klp.creditInfo.lastSignTime !== '无记录') { details['📅 签到时间'] = klp.creditInfo.lastSignTime; } if (klp.creditInfo.lastSignReward !== '无记录') { details['🎁 签到奖励'] = klp.creditInfo.lastSignReward; } details['💰 铁粒余额'] = klp.creditInfo.creditAmount; } if (klp.isAlreadySigned || klp.sign === 'already') { statusTitle = '苦力怕论坛 - 今日已签到'; statusType = 'info'; } else if (klp.sign === 'success') { statusTitle = '苦力怕论坛 - 签到成功'; statusType = 'success'; } else if (klp.sign === 'uncertain') { statusTitle = '苦力怕论坛 - 签到状态不确定'; statusType = 'info'; } else if (klp.errors.length === 0) { statusTitle = '苦力怕论坛 - 签到失败'; statusType = 'info'; } if (!notificationSent) { await showNotificationWithPush(statusType, statusTitle, details); notificationSent = true; } } })();