// ==UserScript==
// @name DSU多站自动签到 (基于Ne-21脚本重构)
// @namespace discuz-dsu-checkin-enhanced
// @source https://github.com/little3tar/discuz-dsu-checkin
// @website https://scriptcat.org/zh-CN/script-show-page/4495
// @version 0.2.3
// @description 支持油猴中文网、Anime字幕论坛、天使动漫论坛的DSU每日自动签到
// @author sakura (基于Ne-21脚本重构)
// @crontab * 1-23 once * *
// @grant GM_notification
// @grant GM_xmlhttpRequest
// @grant GM_log
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @exportcookie domain=.tampermonkey.net.cn
// @exportcookie domain=.bbs.acgrip.com
// @exportcookie domain=.tsdm39.com
// @connect bbs.tampermonkey.net.cn
// @connect bbs.acgrip.com
// @connect www.tsdm39.com
// ==/UserScript==
(function () {
'use strict';
// 站点配置
const SITES = [
{
name: '油猴中文网',
signPageUrl: 'https://bbs.tampermonkey.net.cn/dsu_paulsign-sign.html',
signApiUrl: 'https://bbs.tampermonkey.net.cn/plugin.php?id=dsu_paulsign:sign&operation=qiandao&infloat=1&inajax=1',
referer: 'https://bbs.tampermonkey.net.cn/plugin.php?id=dsu_paulsign:sign',
domain: '.tampermonkey.net.cn',
enabled: true
},
{
name: 'Anime字幕论坛',
signPageUrl: 'https://bbs.acgrip.com/dsu_paulsign-sign.html',
signApiUrl: 'https://bbs.acgrip.com/plugin.php?id=dsu_paulsign:sign&operation=qiandao&infloat=1&inajax=1',
referer: 'https://bbs.acgrip.com/plugin.php?id=dsu_paulsign:sign',
domain: '.bbs.acgrip.com',
enabled: true
},
{
name: '天使动漫论坛',
signPageUrl: 'https://www.tsdm39.com/plugin.php?id=dsu_paulsign:sign',
signApiUrl: 'https://www.tsdm39.com/plugin.php?id=dsu_paulsign:sign&operation=qiandao&infloat=1&inajax=1',
referer: 'https://www.tsdm39.com/plugin.php?id=dsu_paulsign:sign',
domain: '.tsdm39.com',
enabled: true
}
];
// 重试配置
const RETRY_CONFIG = {
maxRetries: 3,
retryDelay: 2000,
timeout: 10000
};
// 签到配置
const SIGN_CONFIG = {
moods: ['kx', 'ng', 'ym', 'wl', 'nu', 'ch', 'fd', 'yl', 'shuai'], // 心情参数(网站规定)
sayings: [
'今天也要加油鸭~',
'签到咯~',
'每日打卡!',
'来签到啦!',
'新的一天开始了',
'日常报到~',
'打卡成功!'
],
requestDelay: 1000, // 请求间隔(毫秒)
reasonMaxLength: 15 // 失败原因最大显示长度
};
// 存储键名
const STORAGE_KEYS = {
SITE_CONFIG: 'site_config',
FAILED_SITES: 'failed_sites',
LAST_SIGN_DATE: 'last_sign_date'
};
// 初始化存储
function initStorage() {
if (!GM_getValue(STORAGE_KEYS.SITE_CONFIG)) {
GM_setValue(STORAGE_KEYS.SITE_CONFIG, SITES);
}
if (!GM_getValue(STORAGE_KEYS.FAILED_SITES)) {
GM_setValue(STORAGE_KEYS.FAILED_SITES, []);
}
if (!GM_getValue(STORAGE_KEYS.LAST_SIGN_DATE)) {
GM_setValue(STORAGE_KEYS.LAST_SIGN_DATE, '');
}
}
// 检查今天是否已签到
function hasSignedToday() {
const lastSignDate = GM_getValue(STORAGE_KEYS.LAST_SIGN_DATE, '');
const today = new Date().toDateString();
return lastSignDate === today;
}
// 更新签到日期
function updateSignDate() {
GM_setValue(STORAGE_KEYS.LAST_SIGN_DATE, new Date().toDateString());
}
// 正则匹配工具函数
function getStr(str, start, end) {
const startIdx = str.indexOf(start);
if (startIdx === -1) return null;
const contentStart = startIdx + start.length;
const endIdx = str.indexOf(end, contentStart);
return endIdx === -1 ? null : str.substring(contentStart, endIdx);
}
// 提取 formhash(支持多种格式)
function extractFormhash(html) {
// 尝试多种匹配模式
const patterns = [
/formhash=([a-f0-9]+)"/i,
/formhash=([a-f0-9]+)'/i,
/formhash["\s]*:["\s]*([a-f0-9]+)/i,
/]*name=["']formhash["'][^>]*value=["']([a-f0-9]+)["']/i
];
for (const pattern of patterns) {
const match = html.match(pattern);
if (match && match[1]) {
return match[1];
}
}
return null;
}
// 提取签到响应消息
function extractSignMessage(html) {
const cleanHtml = html.replace(/\s/g, "");
// 尝试多种提取模式
const patterns = [
/([^<]+)<\/div>/i,
/]*>([^<]+)<\/div>/i,
/CDATA\[([^\]]+)\]\]>/i
];
for (const pattern of patterns) {
const match = cleanHtml.match(pattern);
if (match && match[1]) {
return match[1];
}
}
return null;
}
// 随机获取签到语
function getRandomSaying() {
return SIGN_CONFIG.sayings[Math.floor(Math.random() * SIGN_CONFIG.sayings.length)];
}
// 随机获取心情参数
function getRandomMood() {
return SIGN_CONFIG.moods[Math.floor(Math.random() * SIGN_CONFIG.moods.length)];
}
// 缩短失败原因描述
function shortenReason(message) {
if (!message) return '未知错误';
const msg = message.trim();
if (msg.includes('无法获取formhash') || msg.includes('登录失效')) {
return '登录失效';
}
if (msg.includes('签到时间还没有到') || msg.includes('时间未到')) {
return '时间未到';
}
if (msg.includes('网络错误')) {
return '网络错误';
}
if (msg.includes('请求超时') || msg.includes('获取页面超时') || msg.includes('超时')) {
return '超时';
}
if (msg.includes('获取页面失败') || msg.includes('页面失败')) {
return '页面失败';
}
if (msg.includes('签到结果不确定')) {
return '未知错误';
}
// 默认截断
return msg.length > SIGN_CONFIG.reasonMaxLength ? msg.substring(0, SIGN_CONFIG.reasonMaxLength) + '...' : msg;
}
// 获取站点配置
function getSiteConfig() {
return GM_getValue(STORAGE_KEYS.SITE_CONFIG, SITES);
}
// 获取失败站点列表
function getFailedSites() {
return GM_getValue(STORAGE_KEYS.FAILED_SITES, []);
}
// 更新失败站点列表
function updateFailedSites(failedSites) {
GM_setValue(STORAGE_KEYS.FAILED_SITES, failedSites);
}
// 处理请求错误并决定是否重试
async function handleRequestError(site, errorMsg, retryCount, resolve) {
GM_log(`${site.name} ${errorMsg}`, "warn");
if (retryCount < RETRY_CONFIG.maxRetries) {
setTimeout(async () => {
const result = await signSiteWithRetry(site, retryCount + 1);
resolve(result);
}, RETRY_CONFIG.retryDelay);
} else {
resolve({
success: false,
message: errorMsg,
retried: retryCount
});
}
}
// 带重试的单个站点签到
async function signSiteWithRetry(site, retryCount = 0) {
return new Promise((resolve) => {
GM_log(`开始签到: ${site.name}${retryCount > 0 ? ` (第${retryCount + 1}次重试)` : ''}`, "info");
// 第一步:获取formhash
GM_xmlhttpRequest({
url: site.signPageUrl,
method: 'GET',
timeout: RETRY_CONFIG.timeout,
onload: function (xhr) {
if (xhr.status !== 200) {
handleRequestError(site, `网络错误: ${xhr.status}`, retryCount, resolve);
return;
}
const formhash = extractFormhash(xhr.responseText);
if (!formhash) {
const errorMsg = '无法获取formhash,可能已签到或登录失效';
GM_log(`${site.name} ${errorMsg}`, "warn");
resolve({
success: false,
message: errorMsg,
retried: retryCount
});
return;
}
// 第二步:提交签到
const signData = `formhash=${encodeURIComponent(formhash)}&qdxq=${getRandomMood()}&qdmode=1&todaysay=${encodeURIComponent(getRandomSaying())}&fastreply=0`;
GM_xmlhttpRequest({
method: 'POST',
url: site.signApiUrl,
data: signData,
headers: {
'content-type': 'application/x-www-form-urlencoded',
'Referer': site.referer
},
timeout: RETRY_CONFIG.timeout,
onload: function (signXhr) {
const msg = extractSignMessage(signXhr.responseText);
if (!msg) {
handleRequestError(site, '签到失败,可能登录失效', retryCount, resolve);
return;
}
// 检查返回的消息是否表示真正的签到成功
const successIndicators = ['恭喜您签到成功', '签到成功', '您今日已经签到', '已经签到', '打卡成功'];
const failureIndicators = ['签到时间还没有到', '请在.*后重新进行签到', '时间未到', '需要先登录'];
const isSuccess = successIndicators.some(indicator => msg.includes(indicator));
const isKnownFailure = failureIndicators.some(indicator => new RegExp(indicator).test(msg));
if (isSuccess) {
GM_log(`${site.name} 签到成功: ${msg}`, "info");
resolve({
success: true,
message: msg,
retried: retryCount
});
} else if (isKnownFailure) {
GM_log(`${site.name} 签到失败: ${msg}`, "info");
resolve({
success: false,
message: msg,
retried: retryCount
});
} else {
GM_log(`${site.name} 签到结果未知: ${msg}`, "warn");
resolve({
success: false,
message: `签到结果未知: ${msg}`,
retried: retryCount
});
}
},
onerror: () => handleRequestError(site, '请求失败', retryCount, resolve),
ontimeout: () => handleRequestError(site, '请求超时', retryCount, resolve)
});
},
onerror: () => handleRequestError(site, '获取页面失败', retryCount, resolve),
ontimeout: () => handleRequestError(site, '获取页面超时', retryCount, resolve)
});
});
}
// 批量签到
async function batchSign(forceSign = false) {
// 检查是否今天已签到(除非强制签到)
if (!forceSign && hasSignedToday()) {
GM_log('今天已经签到过,跳过自动签到', "info");
return [];
}
const sites = getSiteConfig().filter(site => site.enabled);
const results = [];
const failedEntries = []; // {name, reason}
GM_log(`开始批量签到,共 ${sites.length} 个站点`, "info");
for (const site of sites) {
try {
const result = await signSiteWithRetry(site);
results.push({
site: site.name,
...result
});
if (!result.success) {
const shortReason = shortenReason(result.message);
failedEntries.push({
name: site.name,
reason: shortReason
});
}
// 延迟一下,避免请求过于频繁
await new Promise(resolve => setTimeout(resolve, SIGN_CONFIG.requestDelay));
} catch (error) {
const errorResult = {
site: site.name,
success: false,
message: `异常错误: ${error}`,
retried: 0
};
results.push(errorResult);
const shortReason = shortenReason(errorResult.message);
failedEntries.push({
name: site.name,
reason: shortReason
});
}
}
// 保存失败站点列表(仅名称)
const failedSiteNames = failedEntries.map(entry => entry.name);
updateFailedSites(failedSiteNames);
// 更新签到日期
updateSignDate();
// 推送汇总通知
const successCount = results.filter(r => r.success).length;
if (failedEntries.length === 0) {
GM_notification('签到完成', `全部 ${successCount} 个站点签到成功!`);
} else {
const failureDescriptions = failedEntries.slice(0, 3).map(entry => `${entry.name}: ${entry.reason}`);
const moreText = failedEntries.length > 3 ? ` 等${failedEntries.length}个` : '';
GM_notification('签到完成', `成功 ${successCount}/${results.length},失败: ${failureDescriptions.join(', ')}${moreText}`);
}
return results;
}
// 立即执行签到(强制重新签到所有站点)
function executeSignNow() {
GM_notification('开始签到', '正在强制重新签到所有站点...');
// 清除之前的失败记录,强制重新签到所有站点
updateFailedSites([]);
// 直接调用batchSign,它内部已经包含通知逻辑
batchSign(true);
}
// 重试失败站点
async function retryFailedSites() {
const failedSites = getFailedSites();
if (failedSites.length === 0) {
GM_notification('重试失败站点', '没有需要重试的失败站点');
return;
}
const sites = getSiteConfig().filter(site => failedSites.includes(site.name));
const results = [];
const stillFailedEntries = []; // {name, reason}
GM_notification('开始重试', `正在重试 ${failedSites.length} 个失败站点`);
for (const site of sites) {
try {
const result = await signSiteWithRetry(site);
results.push({
site: site.name,
...result
});
if (!result.success) {
const shortReason = shortenReason(result.message);
stillFailedEntries.push({
name: site.name,
reason: shortReason
});
}
await new Promise(resolve => setTimeout(resolve, SIGN_CONFIG.requestDelay));
} catch (error) {
const errorResult = {
site: site.name,
success: false,
message: `重试异常: ${error}`,
retried: 0
};
results.push(errorResult);
const shortReason = shortenReason(errorResult.message);
stillFailedEntries.push({
name: site.name,
reason: shortReason
});
}
}
// 更新失败站点列表(仅名称)
const stillFailedSiteNames = stillFailedEntries.map(entry => entry.name);
updateFailedSites(stillFailedSiteNames);
const successCount = results.filter(r => r.success).length;
if (stillFailedEntries.length === 0) {
GM_notification('重试完成', `全部 ${successCount} 个站点签到成功!`);
} else {
const failureDescriptions = stillFailedEntries.slice(0, 3).map(entry => `${entry.name}: ${entry.reason}`);
const moreText = stillFailedEntries.length > 3 ? ` 等${stillFailedEntries.length}个` : '';
GM_notification('重试完成', `成功 ${successCount}/${results.length},失败: ${failureDescriptions.join(', ')}${moreText}`);
}
}
// 注册菜单命令
function registerMenuCommands() {
GM_registerMenuCommand('🚀 立即签到', executeSignNow);
GM_registerMenuCommand('⚡ 重试失败站点', retryFailedSites);
}
// 主函数
function main() {
// 初始化存储
initStorage();
// 注册菜单
registerMenuCommands();
// 自动执行签到(带防重复检查)
GM_log('脚本已加载,检查是否需要自动签到', "info");
batchSign(false).then(results => {
if (results.length > 0) {
const successCount = results.filter(r => r.success).length;
const totalCount = results.length;
GM_log(`自动签到完成: ${successCount}/${totalCount} 成功`, "info");
}
});
GM_log('DSU多站自动签到脚本已加载', "info");
}
// 启动脚本
main();
})();