// ==UserScript== // @name 科研通自动签到 // @namespace https://github.com/Walvez/ablesci-auto-checkin // @version 0.1.2 // @description 在脚本猫后台为科研通执行每日签到;复用浏览器登录态,无需复制 Cookie,也无需保持网页打开。 // @author Walvez // @homepageURL https://github.com/Walvez/ablesci-auto-checkin // @supportURL https://github.com/Walvez/ablesci-auto-checkin/issues // @source https://github.com/Walvez/ablesci-auto-checkin // @icon https://raw.githubusercontent.com/Walvez/ablesci-auto-checkin/main/assets/faviconV2.png // @tag 科研通 // @tag 自动签到 // @tag 后台脚本 // @crontab 5-55/5 * once * * // @grant GM_xmlhttpRequest // @grant GM_notification // @grant GM_openInTab // @grant GM_log // @grant GM_registerMenuCommand // @connect www.ablesci.com // @license MIT // ==/UserScript== const SIGN_URL = "https://www.ablesci.com/user/sign"; const LOGIN_URL = "https://www.ablesci.com/site/login"; const REFERER = "https://www.ablesci.com/"; const REQUEST_TIMEOUT = 15000; const LOG_PREFIX = "[AbleSci]"; const RETRY_HINT = "脚本会在下一个候选时间自动再试;也可修复登录或网络后,通过菜单“立即手动签到”重试。"; const OUTCOME = { SUCCESS: "success", ALREADY_CHECKED: "already_checked", LOGIN_REQUIRED: "login_required", RETRYABLE_FAILURE: "retryable_failure", NON_RETRYABLE_FAILURE: "non_retryable_failure", }; function log(message, level = "info") { GM_log(`${LOG_PREFIX} ${message}`, level); } function sanitizeDisplayText(value, fallback = "") { const text = String(value == null ? "" : value) .replace(/\s+/g, " ") .trim(); if (!text) return fallback; return text.length > 160 ? `${text.slice(0, 157)}...` : text; } function openLoginPage() { GM_openInTab(LOGIN_URL, true); } function notify(title, text, options = {}) { const payload = { title, text: sanitizeDisplayText(text, title), }; if (typeof options.onclick === "function") { payload.onclick = options.onclick; } GM_notification(payload); } function looksLikeLoginHtml(text) { const sample = String(text || "").slice(0, 4000).toLowerCase(); if (!sample) return false; if (!sample.includes("= 500) { return { kind: OUTCOME.RETRYABLE_FAILURE, message: `科研通服务暂时异常(HTTP ${status})。`, }; } if (status >= 400 && status < 500) { return { kind: OUTCOME.NON_RETRYABLE_FAILURE, message: `请求被拒绝(HTTP ${status})。`, }; } if (status < 200 || status >= 300) { return { kind: OUTCOME.RETRYABLE_FAILURE, message: `请求失败(HTTP ${status})。`, }; } if (looksLikeLoginHtml(rawText) || isLoginMessage(rawText)) { return { kind: OUTCOME.LOGIN_REQUIRED, message: "登录状态已失效,请在当前浏览器重新登录科研通。", }; } const payload = extractJson(response); if (!payload || typeof payload !== "object" || Array.isArray(payload)) { return { kind: OUTCOME.RETRYABLE_FAILURE, message: "签到接口返回了无法识别的非 JSON 响应。", }; } const msg = sanitizeDisplayText(payload.msg || payload.message || ""); if (isLoginMessage(msg)) { return { kind: OUTCOME.LOGIN_REQUIRED, message: msg || "登录状态已失效,请在当前浏览器重新登录科研通。", }; } if (isAlreadyCheckedMessage(msg)) { return { kind: OUTCOME.ALREADY_CHECKED, message: msg || "今天已经签到。", }; } if (Number(payload.code) === 0) { return { kind: OUTCOME.SUCCESS, message: buildSuccessText(payload), }; } if (msg) { return { kind: OUTCOME.NON_RETRYABLE_FAILURE, message: msg, }; } return { kind: OUTCOME.RETRYABLE_FAILURE, message: "签到接口返回了无法识别的业务响应。", }; } function requestSign() { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: SIGN_URL, timeout: REQUEST_TIMEOUT, responseType: "json", anonymous: false, headers: { Accept: "application/json, text/javascript, */*; q=0.01", "X-Requested-With": "XMLHttpRequest", Referer: REFERER, }, onload(response) { resolve(response); }, onerror() { reject(Object.assign(new Error("网络请求失败"), { kind: OUTCOME.RETRYABLE_FAILURE })); }, ontimeout() { reject(Object.assign(new Error("网络请求超时"), { kind: OUTCOME.RETRYABLE_FAILURE })); }, onabort() { reject(Object.assign(new Error("网络请求被终止"), { kind: OUTCOME.RETRYABLE_FAILURE })); }, }); }); } function notifyOutcome(outcome) { if (outcome.kind === OUTCOME.SUCCESS) { notify("科研通签到成功", outcome.message); return; } if (outcome.kind === OUTCOME.ALREADY_CHECKED) { notify("科研通今日已签到", outcome.message); return; } if (outcome.kind === OUTCOME.LOGIN_REQUIRED) { notify("科研通需要重新登录", `${outcome.message} ${RETRY_HINT}`, { onclick: openLoginPage, }); return; } notify("科研通签到失败", `${outcome.message} ${RETRY_HINT}`); } function isCompleted(kind) { return kind === OUTCOME.SUCCESS || kind === OUTCOME.ALREADY_CHECKED; } async function runCheckin() { log("开始签到"); let response; try { response = await requestSign(); } catch (error) { const outcome = { kind: error.kind || OUTCOME.RETRYABLE_FAILURE, message: sanitizeDisplayText(error.message, "签到请求失败"), }; log(`分类结果:${outcome.kind};${outcome.message}`, "error"); notifyOutcome(outcome); const failure = new Error(outcome.message); failure.kind = outcome.kind; failure.alreadyNotified = true; throw failure; } const outcome = classifyResponse(response); log(`分类结果:${outcome.kind}`); notifyOutcome(outcome); if (isCompleted(outcome.kind)) { return outcome; } const failure = new Error(outcome.message); failure.kind = outcome.kind; failure.alreadyNotified = true; throw failure; } function registerMenus() { if (typeof GM_registerMenuCommand !== "function") return; GM_registerMenuCommand("立即手动签到", () => { return runCheckin() .then((outcome) => { log(`手动签到完成:${outcome.kind}`); return outcome; }) .catch((error) => { log(`手动签到失败:${error && error.message ? error.message : String(error)}`, "error"); // Contain failures inside the menu callback; do not rethrow. }); }); GM_registerMenuCommand("打开科研通登录页", () => { openLoginPage(); }); } registerMenus(); return runCheckin().catch((error) => { if (!error || !error.alreadyNotified) { const message = sanitizeDisplayText(error && error.message, "签到失败"); log(`未分类异常:${message}`, "error"); notify("科研通签到失败", `${message} ${RETRY_HINT}`); } throw error; });