// ==UserScript== // @name EPIC白嫖小助手 // @description 每1小时检测一次是否有可以白嫖的epic游戏,支持多数据源回退 // @namespace https://bbs.tampermonkey.net.cn/ // @version 0.3.5 // @author CodFrm, Cosil // @grant GM_xmlhttpRequest // @grant GM_notification // @grant GM_closeNotification // @grant GM_openInTab // @grant GM_getValue // @grant GM_setValue // @grant GM_setClipboard // @grant GM_registerMenuCommand // @storageName find_epic_free_games // @connect store-site-backend-static.ak.epicgames.com // @connect graphql.epicgames.com // @connect store-site-backend-static-ipv4.ak.epicgames.com // @connect www.epicgames.com // @connect store.epicgames.com // @crontab 0 * * * * // @license GPLv3 // ==/UserScript== (function () { 'use strict'; // 如需重置通知记录,请取消下面一行的注释,运行一次后重新注释 //GM_setValue('notified_free_games', {}); const API_ENDPOINTS = [ 'https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions?locale=zh-CN&country=CN&allowCountries=CN,HK', 'https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions?locale=zh-CN&country=CN&allowCountries=CN,HK' ]; // 基础商店地址,使用无语言后缀的通用路径,会自动跳转到用户语言 const STORE_BASE = "https://store.epicgames.com/p/"; const NOTIFIED_KEY = "notified_free_games"; const MAX_NOTIFIED_ITEMS = 30; function request(option) { return new Promise((resolve, reject) => { option.timeout = 15000; // 15秒超时 option.onload = (res) => { if (res.status === 200) { resolve(res); } else { reject(new Error(`HTTP ${res.status}`)); } }; option.onerror = (e) => reject(new Error('Network error')); option.ontimeout = () => reject(new Error('Timeout')); GM_xmlhttpRequest(option); }); } // 多源回退请求函数 async function fetchFromEpicAPI(urls) { for (const url of urls) { try { console.log(`尝试请求API: ${url}`); const response = await request({ url: url, method: 'GET', responseType: 'json' }); if (response.status === 200) { return response; } throw new Error(`请求失败,状态码: ${response.status}`); } catch (error) { console.warn(`API请求失败: ${url}`, error); continue; } } throw new Error('所有API端点都无法访问'); } // 清理过期的通知记录 function cleanExpiredNotified(records) { const now = Date.now(); return Object.entries(records).reduce((acc, [id, data]) => { if (!data.promoEnd || new Date(data.promoEnd).getTime() > now) { acc[id] = data; } return acc; }, {}); } // 限制通知记录数量 function limitNotified(records, max) { const entries = Object.entries(records); if (entries.length <= max) return records; entries.sort((a, b) => b[1].notifyTime - a[1].notifyTime); return Object.fromEntries(entries.slice(0, max)); } async function checkFreeGames() { try { // 1. 获取免费游戏列表 const resp = await fetchFromEpicAPI(API_ENDPOINTS); const elements = resp.response?.data?.Catalog?.searchStore?.elements; if (!elements) { console.warn("无法解析游戏数据"); return; } const now = new Date(); const freeGames = []; for (const game of elements) { if (game.status !== "ACTIVE") continue; let isFree = false; let promoEnd = null; // 检查促销信息 const promoGroups = game.promotions?.promotionalOffers; if (promoGroups && Array.isArray(promoGroups)) { for (const group of promoGroups) { for (const offer of group.promotionalOffers || []) { if (offer.discountSetting?.discountPercentage === 0) { const start = new Date(offer.startDate); const end = new Date(offer.endDate); if (start <= now && end > now) { isFree = true; promoEnd = end.toISOString(); break; } } } if (isFree) break; } } // 兜底:原价>0 且 现价=0 if (!isFree && game.price?.totalPrice?.originalPrice > 0 && game.price.totalPrice.discountPrice === 0) { isFree = true; } if (isFree) { // 提取真正的产品页面 slug let realSlug = game.productSlug || game.urlSlug; // 优先使用 productSlug const mappings = game.catalogNs?.mappings; if (mappings && Array.isArray(mappings)) { // 查找 pageType 为 "productHome" 的映射 const productMapping = mappings.find(m => m.pageType === "productHome"); if (productMapping && productMapping.pageSlug) { realSlug = productMapping.pageSlug; } } // 如果仍然没有找到,尝试从 offerMappings 获取 if (!realSlug || realSlug === game.urlSlug) { const offerMappings = game.catalogNs?.offerMappings; if (offerMappings && Array.isArray(offerMappings) && offerMappings.length > 0) { realSlug = offerMappings[0].pageSlug || realSlug; } } console.log(`游戏: ${game.title}, 原始urlSlug: ${game.urlSlug}, 真实slug: ${realSlug}`); freeGames.push({ id: game.id, title: game.title, urlSlug: realSlug, image: game.keyImages?.find(img => img.type === "DieselStoreFrontWide")?.url || "", promoEnd }); } } console.log("找到免费游戏:", freeGames); if (freeGames.length === 0) return; // 2. 读取通知记录 let notifiedGames = GM_getValue(NOTIFIED_KEY, {}); notifiedGames = cleanExpiredNotified(notifiedGames); notifiedGames = limitNotified(notifiedGames, MAX_NOTIFIED_ITEMS); // 3. 筛选未通知的游戏(同一促销期内不重复通知) const gamesToNotify = freeGames.filter(game => { const notified = notifiedGames[game.id]; if (notified && Date.now() - notified.notifyTime < 7 * 24 * 60 * 60 * 1000) { // 促销期相同(或均无法确定促销期),视为同一期,不再通知 if (!game.promoEnd || !notified.promoEnd || notified.promoEnd === game.promoEnd) { return false; } } return true; }); if (gamesToNotify.length === 0) return; const msg = gamesToNotify.map(g => g.title).join("; "); GM_notification({ title: "🎁 Epic 限时免费游戏", text: msg, image: gamesToNotify[0].image || undefined, buttons: [ { title: "知道了" }, { title: "一键领取" } ], onclick(event) { if (event && event.buttonClickIndex === 1) { gamesToNotify.forEach(game => { const url = STORE_BASE + game.urlSlug; try { GM_openInTab(url, false); } catch (e) { console.error("GM_openInTab 失败:", e); GM_setClipboard(url, 'text'); } }); } // 用户点击任意按钮后标记已通知,未点击则不标记,下次继续提醒 for (const game of gamesToNotify) { notifiedGames[game.id] = { title: game.title, notifyTime: Date.now(), promoEnd: game.promoEnd }; } GM_setValue(NOTIFIED_KEY, limitNotified(notifiedGames, MAX_NOTIFIED_ITEMS)); }, timeout: 15 * 1000 }); } catch (e) { console.error("EPIC白嫖助手出错:", e); GM_notification({ title: "EPIC白嫖助手", text: "检测失败: " + e.message, timeout: 5000 }); } } // 启动检测 GM_registerMenuCommand("🔍 手动检测EPIC白嫖", () => checkFreeGames()); return checkFreeGames(); })();