// ==UserScript== // @name 苦力怕论坛自动登录签到 // @namespace https://bbs.tampermonkey.net.cn/ // @version 0.9.6 // @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 // @match https://klpbbs.com/* // ==/UserScript== /* ==UserConfig== Config: klp_username: title: 苦力怕论坛用户名 type: text default: "" klp_password: title: 苦力怕论坛密码 type: text password: true ==/UserConfig== */ const MAX_RETRIES = 3; const RETRY_DELAY = 2000; function showNotification(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' }); } 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 = { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': `https://klpbbs.com/`, 'User-Agent': navigator.userAgent }; const finalHeaders = {...defaultHeaders, ...headers}; GM_xmlhttpRequest({ method: method, url: url, data: data, headers: finalHeaders, onload: (res) => { if (res.status >= 200 && res.status < 300) { 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) { 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) { 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) { // 出错时返回未签到 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) { 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('获取签到令牌失败'); } } let signPostUrl, signData; // 手机版和桌面版使用不同的签到参数 if (type === 'mobile') { // 手机版使用ajax方式签到 signPostUrl = `https://klpbbs.com/plugin.php?id=k_misign:sign&operation=qiandao&format=text&formhash=${formhash}&signsubmit=yes&inajax=1`; signData = `formhash=${formhash}&signsubmit=yes&inajax=1`; } else { // 桌面版 signPostUrl = `${signUrl}&operation=qiandao&format=text`; signData = `formhash=${formhash}&signsubmit=yes`; } const signRes = await fetchAPI( signPostUrl, 'POST', signData ); // 增强的签到成功判断 const successIndicators = [ /签到成功/, /您的签到排名/, /已连续签到/, /已签到/, /window\.location\.reload/ ]; const failIndicators = [ /签到失败/, /您今天已经签到/, /formhash错误/, ]; const isSuccess = successIndicators.some(indicator => indicator.test(signRes.text)); const isFail = failIndicators.some(indicator => indicator.test(signRes.text)); // 无论响应如何,都重新获取签到页面来确认状态 await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒让服务器处理 const finalStatus = await checkSignStatus(versionInfo); if (finalStatus) { return 'success'; } else if (isSuccess) { return 'success'; } else if (isFail || isAlreadySigned) { return 'already'; } else { return 'fail'; } } catch (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) { 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", '') }; const results = { klp: { login: false, sign: false, creditInfo: null, version: 'unknown', isAlreadyLoggedIn: false, isAlreadySigned: false, errors: [] } }; 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) { results.klp.errors.push(error.message); } } else { results.klp.errors.push('苦力怕论坛账号密码配置不完整'); } const klp = results.klp; // 错误信息单独通知 if (klp.errors.length > 0) { showNotification('error', '苦力怕论坛签到错误', {错误信息: klp.errors.join('; ')}); } // 正常状态通知(无论是否有错误都显示) 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 (klp.errors.length === 0 || klp.creditInfo) { showNotification(statusType, statusTitle, details); } })();