前情提要
你是否因为忘记上次金币卡的使用时间导致想用道具发现还在冷却,等到下次想起时,时间已经延后,一天一天逐渐累积导致下周到了还有没用掉的金币卡?
或者你只是单纯不想检查自己还有没有可用的金币卡,想着有没有自动使用的方式
如果以上问题困扰着你,那么这个脚本就是来帮助你的
在安装脚本后,登入泥潭时会静默检查,检测到有道具和使用次数便会自动帮你使用,全程无感 - // ==UserScript==
- // [url=home.php?mod=space&uid=668096]@Name[/url] 金币来自动使用
- // @namespace https://www.gamemale.com/
- // @version 0.3.0
- // @description 自动使用“金币来”道具,滚动 7 天内使用未满 3 次则自动使用到上限。
- // @author brine
- // [url=home.php?mod=space&uid=700810]@Match[/url] https://www.gamemale.com/*
- // @run-at document-idle
- // @grant none
- // ==/UserScript==
- (function () {
- "use strict";
- const CONFIG = {
- magicName: "金币来",
- weeklyLimit: 3,
- rollingWindowMs: 7 * 24 * 60 * 60 * 1000,
- checkIntervalMs: 6 * 60 * 60 * 1000,
- lockMs: 2 * 60 * 1000,
- requestTimeoutMs: 15000,
- submitGapMs: 1400,
- unlockJitterMs: 2 * 60 * 1000,
- debug: false,
- };
- const BOX_URL = "/home.php?mod=magic&action=mybox";
- const USE_LOG_URL = "/home.php?mod=magic&action=log&operation=uselog";
- const STATE_KEY = "gm_jinbilai_auto_use_state_v3";
- const LOCK_KEY = "gm_jinbilai_auto_use_lock_v1";
- if (window.top !== window.self) return;
- if (location.hostname !== "www.gamemale.com") return;
- const log = (...args) => {
- if (CONFIG.debug) console.info("[GM 金币来]", ...args);
- };
- const now = () => Date.now();
- const readJSON = (key, fallback) => {
- try {
- const raw = localStorage.getItem(key);
- return raw ? JSON.parse(raw) : fallback;
- } catch (_) {
- return fallback;
- }
- };
- const writeJSON = (key, value) => {
- try {
- localStorage.setItem(key, JSON.stringify(value));
- } catch (_) {
- // Ignore storage failures; server-side checks still protect the action.
- }
- };
- const absoluteURL = (url) => new URL(url, location.origin).href;
- const fetchText = async (url, options = {}) => {
- const controller = new AbortController();
- const timer = window.setTimeout(
- () => controller.abort(),
- CONFIG.requestTimeoutMs,
- );
- try {
- const response = await fetch(absoluteURL(url), {
- credentials: "same-origin",
- redirect: "follow",
- signal: controller.signal,
- ...options,
- headers: {
- "X-Requested-With": "XMLHttpRequest",
- ...(options.headers || {}),
- },
- });
- return await response.text();
- } finally {
- window.clearTimeout(timer);
- }
- };
- const parseHTML = (html) =>
- new DOMParser().parseFromString(html, "text/html");
- const sleep = (ms) =>
- new Promise((resolve) => window.setTimeout(resolve, ms));
- const isLoggedOutPage = (doc) => {
- const text = doc.body ? doc.body.textContent || "" : "";
- return /尚未登录|请先登录|not_loggedin|member\.php\?mod=logging/.test(text);
- };
- const acquireLock = () => {
- const token = `${now()}-${Math.random().toString(36).slice(2)}`;
- const current = readJSON(LOCK_KEY, null);
- if (current && current.expiresAt && current.expiresAt > now()) return null;
- writeJSON(LOCK_KEY, { token, expiresAt: now() + CONFIG.lockMs });
- const stored = readJSON(LOCK_KEY, null);
- return stored && stored.token === token ? token : null;
- };
- const releaseLock = (token) => {
- const current = readJSON(LOCK_KEY, null);
- if (current && current.token === token) localStorage.removeItem(LOCK_KEY);
- };
- const shouldCheck = () => {
- const state = readJSON(STATE_KEY, {});
- if (state.nextCheckAt && state.nextCheckAt > now()) return false;
- return true;
- };
- const scheduleNextCheck = (
- extra = {},
- nextCheckAt = now() + CONFIG.checkIntervalMs,
- ) => {
- writeJSON(STATE_KEY, {
- ...readJSON(STATE_KEY, {}),
- ...extra,
- nextCheckAt,
- checkedAt: now(),
- });
- };
- const findMagicItem = (doc) => {
- const candidates = Array.from(
- doc.querySelectorAll("li, tr, .bm, .xld, .mgcl > *"),
- );
- for (const node of candidates) {
- const text = node.textContent || "";
- if (!text.includes(CONFIG.magicName)) continue;
- const useLink = Array.from(node.querySelectorAll("a[href]")).find(
- (anchor) => {
- const href = anchor.getAttribute("href") || "";
- const label = anchor.textContent || "";
- return (
- href.includes("mod=magic") &&
- href.includes("action=mybox") &&
- href.includes("operation=use") &&
- /使用|use/i.test(label + href)
- );
- },
- );
- const source = useLink ? useLink.href : node.innerHTML;
- const magicId = extractMagicId(source);
- const count = extractOwnedCount(text);
- return { magicId, useHref: useLink ? useLink.href : "", count, text };
- }
- const byAlt = Array.from(
- doc.querySelectorAll(`img[alt="${CONFIG.magicName}"]`),
- );
- for (const img of byAlt) {
- const node = img.closest("li, tr, .bm, .xld, .mgcl > *");
- if (!node) continue;
- const link = node.querySelector(
- 'a[href*="operation=use"][href*="magicid="]',
- );
- return {
- magicId: extractMagicId(link ? link.href : node.innerHTML),
- useHref: link ? link.href : "",
- count: extractOwnedCount(node.textContent || ""),
- text: node.textContent || "",
- };
- }
- return null;
- };
- const extractMagicId = (source) => {
- const decoded = String(source || "").replace(/&/g, "&");
- const match =
- decoded.match(/[?&]magicid=(\d+)/i) ||
- decoded.match(/magicid['"]?\s*[:=]\s*['"]?(\d+)/i);
- return match ? match[1] : "";
- };
- const extractOwnedCount = (text) => {
- const normalized = String(text || "").replace(/\s+/g, " ");
- const match = normalized.match(/(?:数量|数目|num)\D{0,8}(\d+)/i);
- return match ? Number(match[1]) : null;
- };
- const parseDateCell = (text) => {
- const trimmed = String(text || "").trim();
- const normalized = trimmed.replace(/\s+/g, " ");
- const nowDate = new Date();
- let match = normalized.match(
- /(\d{4})[-/年.](\d{1,2})[-/月.](\d{1,2})日?\s*(\d{1,2}):(\d{2})(?::(\d{2}))?/,
- );
- if (match) {
- return new Date(
- Number(match[1]),
- Number(match[2]) - 1,
- Number(match[3]),
- Number(match[4]),
- Number(match[5]),
- Number(match[6] || 0),
- );
- }
- match = normalized.match(
- /(\d{1,2})[-/月.](\d{1,2})日?\s*(\d{1,2}):(\d{2})(?::(\d{2}))?/,
- );
- if (match) {
- return new Date(
- nowDate.getFullYear(),
- Number(match[1]) - 1,
- Number(match[2]),
- Number(match[3]),
- Number(match[4]),
- Number(match[5] || 0),
- );
- }
- match = normalized.match(/昨天\s*(\d{1,2}):(\d{2})(?::(\d{2}))?/);
- if (match) {
- const date = new Date(
- nowDate.getFullYear(),
- nowDate.getMonth(),
- nowDate.getDate() - 1,
- );
- date.setHours(
- Number(match[1]),
- Number(match[2]),
- Number(match[3] || 0),
- 0,
- );
- return date;
- }
- match = normalized.match(/今天\s*(\d{1,2}):(\d{2})(?::(\d{2}))?/);
- if (match) {
- const date = new Date(
- nowDate.getFullYear(),
- nowDate.getMonth(),
- nowDate.getDate(),
- );
- date.setHours(
- Number(match[1]),
- Number(match[2]),
- Number(match[3] || 0),
- 0,
- );
- return date;
- }
- return null;
- };
- const getRecentUses = (doc) => {
- const windowStart = new Date(now() - CONFIG.rollingWindowMs);
- const rows = Array.from(doc.querySelectorAll("table.dt tr, table tr"));
- const dates = [];
- for (const row of rows) {
- const text = row.textContent || "";
- if (!text.includes(CONFIG.magicName)) continue;
- const cells = Array.from(row.querySelectorAll("td, th"));
- const date =
- cells.map((cell) => parseDateCell(cell.textContent)).find(Boolean) ||
- parseDateCell(text);
- if (date && date >= windowStart) dates.push(date);
- }
- return dates.sort((left, right) => left.getTime() - right.getTime());
- };
- const getNextRollingSlotAt = (recentUses, addedUses = 0) => {
- const dates = [...recentUses];
- for (let index = 0; index < addedUses; index += 1) {
- dates.push(new Date(now() + index * CONFIG.submitGapMs));
- }
- dates.sort((left, right) => left.getTime() - right.getTime());
- if (dates.length < CONFIG.weeklyLimit) return now() + CONFIG.checkIntervalMs;
- return dates[0].getTime() + CONFIG.rollingWindowMs + CONFIG.unlockJitterMs;
- };
- const getFormHash = (doc) => {
- const input = doc.querySelector('input[name="formhash"]');
- if (input && input.value) return input.value;
- const html = doc.documentElement ? doc.documentElement.innerHTML : "";
- const match = html.match(/formhash[=:]['"]?([0-9a-f]{8,})/i);
- return match ? match[1] : "";
- };
- const getUseForm = async (magicId, useHref) => {
- const href =
- useHref ||
- `${BOX_URL}&operation=use&magicid=${encodeURIComponent(magicId)}`;
- const html = await fetchText(href);
- const doc = parseHTML(html);
- if (isLoggedOutPage(doc)) return null;
- const form = doc.querySelector('form#magicform, form[action*="mod=magic"]');
- const formHash = getFormHash(doc);
- if (!formHash) return null;
- const action = form
- ? form.getAttribute("action") || `${BOX_URL}&infloat=yes`
- : `${BOX_URL}&infloat=yes`;
- const data = new URLSearchParams();
- if (form) {
- for (const field of Array.from(
- form.querySelectorAll("input, select, textarea"),
- )) {
- if (!field.name || field.disabled) continue;
- if (
- (field.type === "checkbox" || field.type === "radio") &&
- !field.checked
- )
- continue;
- data.set(field.name, field.value || "");
- }
- }
- data.set("formhash", formHash);
- data.set("handlekey", data.get("handlekey") || "magics");
- data.set("operation", "use");
- data.set("magicid", magicId);
- data.set("usesubmit", "true");
- return { action, data };
- };
- const submitUse = async (magicId, useHref) => {
- const useForm = await getUseForm(magicId, useHref);
- if (!useForm) return { ok: false, reason: "missing-use-form" };
- const html = await fetchText(useForm.action, {
- method: "POST",
- body: useForm.data,
- headers: {
- "Content-Type": "application/x-www-form-urlencoded",
- },
- });
- const success =
- /succeedhandle|使用|成功|showDialog|showCreditPrompt/i.test(html) &&
- !/outofperoid|超过|已达到|上限|nopermission|失败|错误/i.test(html);
- return { ok: success, reason: success ? "used" : "server-rejected", html };
- };
- const useUntilLimit = async (magic, recentUses) => {
- const remainingWeeklyUses = Math.max(
- 0,
- CONFIG.weeklyLimit - recentUses.length,
- );
- const ownedLimit = Number.isFinite(magic.count)
- ? Math.max(0, magic.count)
- : remainingWeeklyUses;
- const targetUses = Math.min(remainingWeeklyUses, ownedLimit);
- let successCount = 0;
- let lastResult = { ok: false, reason: "nothing-to-use" };
- for (let index = 0; index < targetUses; index += 1) {
- if (index > 0) await sleep(CONFIG.submitGapMs);
- lastResult = await submitUse(magic.magicId, magic.useHref);
- if (!lastResult.ok) break;
- successCount += 1;
- }
- return {
- ...lastResult,
- successCount,
- targetUses,
- reason:
- successCount === targetUses
- ? "used-to-limit"
- : successCount > 0
- ? "partial-used"
- : lastResult.reason,
- };
- };
- const run = async () => {
- if (!shouldCheck()) return;
- const token = acquireLock();
- if (!token) return;
- try {
- const boxDoc = parseHTML(await fetchText(BOX_URL));
- if (isLoggedOutPage(boxDoc)) {
- scheduleNextCheck({ lastResult: "not-logged-in" });
- return;
- }
- const magic = findMagicItem(boxDoc);
- if (!magic || !magic.magicId || magic.count === 0) {
- scheduleNextCheck({ lastResult: "no-magic" });
- return;
- }
- const logDoc = parseHTML(await fetchText(USE_LOG_URL));
- if (isLoggedOutPage(logDoc)) {
- scheduleNextCheck({ lastResult: "not-logged-in" });
- return;
- }
- const recentUses = getRecentUses(logDoc);
- if (recentUses.length >= CONFIG.weeklyLimit) {
- scheduleNextCheck(
- {
- lastResult: "rolling-limit",
- usedInRollingWindow: recentUses.length,
- },
- getNextRollingSlotAt(recentUses),
- );
- return;
- }
- const result = await useUntilLimit(magic, recentUses);
- scheduleNextCheck(
- {
- lastResult: result.reason,
- usedCount: result.successCount,
- targetUses: result.targetUses,
- lastUseAt:
- result.successCount > 0 ? now() : readJSON(STATE_KEY, {}).lastUseAt,
- usedInRollingWindowBeforeUse: recentUses.length,
- },
- getNextRollingSlotAt(recentUses, result.successCount),
- );
- log(result.reason);
- } catch (error) {
- log(error);
- scheduleNextCheck({
- lastResult: "error",
- lastError: String(error && error.message ? error.message : error),
- });
- } finally {
- releaseLock(token);
- }
- };
- window.setTimeout(run, 3000 + Math.floor(Math.random() * 5000));
- })();
复制代码
|
|