统一的游戏 Key 提取与领取辅助脚本,聚合了 Steam / IndieGala / itch.io。
// ==UserScript==
// @name RedeemHelper
// @namespace https://github.com/HCLonely/RedeemHelper
// @author HCLonely
// @description 统一的游戏 Key 提取与领取辅助脚本,聚合了 Steam / IndieGala / itch.io。
// @description:en Unified helper for extracting and redeeming game keys.
// @version 4.0.2
// @supportURL https://github.com/HCLonely/RedeemHelper/issues
// @homepageURL https://github.com/HCLonely/RedeemHelper
// @icon https://github.com/HCLonely/RedeemHelper/blob/main/icon.ico?raw=true
// @tag games
// @match *://*/*
// @exclude *://store.steampowered.com/widget/*
// @exclude *://*googleads*
// @grant GM_setClipboard
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_cookie
// @run-at document-idle
// @connect www.gog.com
// @connect www.indiegala.com
// @connect itch.io
// @connect store.steampowered.com
// @connect login.steampowered.com
// @connect *
// ==/UserScript==
"use strict";
(() => {
// src/shared/trusted-types.ts
if (typeof trustedTypes !== "undefined" && trustedTypes.createPolicy) {
const policy = trustedTypes.createPolicy("tampermonkey-fix", {
createHTML: (input) => input,
createScript: (input) => input,
createScriptURL: (input) => input
});
const origInnerHTML = Object.getOwnPropertyDescriptor(Element.prototype, "innerHTML");
Object.defineProperty(Element.prototype, "innerHTML", {
get: origInnerHTML.get,
set: function(val) {
if (typeof val === "string") {
origInnerHTML.set.call(this, policy.createHTML(val));
} else {
origInnerHTML.set.call(this, val);
}
},
configurable: true,
enumerable: true
});
const origOuterHTML = Object.getOwnPropertyDescriptor(Element.prototype, "outerHTML");
Object.defineProperty(Element.prototype, "outerHTML", {
get: origOuterHTML.get,
set: function(val) {
if (typeof val === "string") {
origOuterHTML.set.call(this, policy.createHTML(val));
} else {
origOuterHTML.set.call(this, val);
}
},
configurable: true,
enumerable: true
});
const origInsertAdjacentHTML = Element.prototype.insertAdjacentHTML;
Element.prototype.insertAdjacentHTML = function(position, text) {
origInsertAdjacentHTML.call(this, position, policy.createHTML(text));
};
}
// src/shared/http.ts
function request(options) {
return new Promise((resolve) => {
const finish = (response, error) => {
const status = response?.status ?? 0;
const statusText = response?.statusText ?? (error ? "Error" : "");
let responseData = response?.response;
if (options.responseType === "json") {
if (typeof responseData !== "object" && response?.responseText) {
try {
responseData = JSON.parse(response.responseText);
} catch (_e) {
}
}
}
resolve({
ok: status >= 200 && status < 300,
status,
statusText,
data: responseData,
text: response?.responseText,
response,
error
});
};
try {
GM_xmlhttpRequest({
timeout: 3e4,
...options,
onload: (response) => finish(response),
onerror: (response) => finish(response, response),
ontimeout: (response) => finish(response, new Error("Request timed out")),
onabort: (response) => finish(response, new Error("Request aborted"))
});
} catch (error) {
finish(null, error);
}
});
}
// src/shared/observer.ts
function mountObserver(callback) {
const observer4 = new MutationObserver(callback);
observer4.observe(document.body || document.documentElement, {
childList: true,
subtree: true,
characterData: true
});
callback();
return observer4;
}
// src/shared/ui.ts
var activeModal = null;
var stylesInjected = false;
var MODAL_STYLES = `
.rh-modal-overlay {
position: fixed;
inset: 0;
background: rgba(17, 24, 39, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2147483647;
padding: 12px;
box-sizing: border-box;
}
.rh-modal {
width: min(92vw, 460px);
max-height: 85vh;
overflow: auto;
border-radius: 12px;
background: #ffffff;
color: #111827;
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.24);
padding: 20px;
box-sizing: border-box;
font-family: inherit;
}
.rh-modal-icon {
margin: 0 0 10px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
display: inline-flex;
align-items: center;
padding: 3px 8px;
border-radius: 999px;
background: #e5e7eb;
color: #374151;
}
.rh-modal-title {
font-size: 18px;
font-weight: 600;
margin: 0 0 10px;
color: #111827;
}
.rh-modal-text,
.rh-modal-content {
margin: 0 0 16px;
line-height: 1.6;
font-size: 14px;
color: #374151;
word-break: break-word;
}
.rh-modal-content input:not([type]),
.rh-modal-content input[type="text"],
.rh-modal-content input[type="password"],
.rh-modal-content input[type="number"],
.rh-modal-content textarea,
.rh-modal-content select {
width: 100%;
max-width: 100%;
min-width: 0;
box-sizing: border-box;
color: #111827;
background: #ffffff;
border: 1px solid #d1d5db;
border-radius: 8px;
padding: 8px 10px;
font-size: 14px;
line-height: 1.4;
}
.rh-modal-content input:not([type]):focus,
.rh-modal-content input[type="text"]:focus,
.rh-modal-content input[type="password"]:focus,
.rh-modal-content input[type="number"]:focus,
.rh-modal-content textarea:focus,
.rh-modal-content select:focus {
outline: 2px solid #93c5fd;
outline-offset: 1px;
border-color: #60a5fa;
}
.rh-modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
flex-wrap: wrap;
}
.rh-modal-button {
border: 1px solid transparent;
border-radius: 8px;
padding: 8px 14px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
line-height: 1.4;
}
.rh-modal-button--primary {
background: #2563eb;
color: #ffffff;
}
.rh-modal-button--secondary {
background: #f3f4f6;
border-color: #d1d5db;
color: #374151;
}
.rh-modal-button--danger {
background: #dc2626;
color: #ffffff;
}
.rh-modal--success {
border-top: 3px solid #16a34a;
}
.rh-modal--error {
border-top: 3px solid #dc2626;
}
.rh-modal--warning {
border-top: 3px solid #ea580c;
}
.rh-modal--info {
border-top: 3px solid #2563eb;
}
.rh-modal-icon--success {
background: #dcfce7;
color: #166534;
}
.rh-modal-icon--error {
background: #fee2e2;
color: #991b1b;
}
.rh-modal-icon--warning {
background: #ffedd5;
color: #9a3412;
}
.rh-modal-icon--info {
background: #dbeafe;
color: #1d4ed8;
}
`;
function getModalTone(icon) {
if (icon === "success" || icon === "error" || icon === "warning" || icon === "info") {
return icon;
}
return void 0;
}
function getModalToneClass(icon) {
const tone = getModalTone(icon);
return tone ? `rh-modal--${tone}` : "";
}
function getButtonRoleClass(key, tone) {
if (key === "confirm") {
return tone === "error" ? "rh-modal-button--danger" : "rh-modal-button--primary";
}
return "rh-modal-button--secondary";
}
function normalizeOptions(optionsOrTitle, text, icon) {
if (typeof optionsOrTitle === "string") {
return {
title: optionsOrTitle,
text,
icon
};
}
return optionsOrTitle;
}
function injectStyles() {
if (stylesInjected) return;
const style = document.createElement("style");
style.textContent = MODAL_STYLES;
const mountTarget = document.head ?? document.documentElement;
mountTarget.appendChild(style);
stylesInjected = true;
}
function closeActiveModal(value) {
if (!activeModal) return;
const current = activeModal;
activeModal = null;
document.removeEventListener("keydown", current.escHandler);
current.overlay.remove();
current.resolve(value);
}
function renderModal(options) {
if (!activeModal) return;
const { iconEl, titleEl, textEl, contentEl, actionsEl } = activeModal;
const opts = options;
const title = opts.titleText ?? opts.title ?? "";
titleEl.textContent = title;
titleEl.style.display = title ? "" : "none";
iconEl.className = `rh-modal-icon${opts.icon ? ` rh-modal-icon--${opts.icon}` : ""}`;
iconEl.textContent = opts.icon ?? "";
iconEl.style.display = opts.icon ? "" : "none";
textEl.textContent = opts.text ?? "";
textEl.style.display = textEl.textContent ? "" : "none";
contentEl.innerHTML = "";
if (typeof opts.content === "string") {
contentEl.textContent = opts.content;
} else if (opts.content instanceof HTMLElement) {
contentEl.appendChild(opts.content);
}
contentEl.style.display = contentEl.textContent || contentEl.childNodes.length ? "" : "none";
const toneClass = getModalToneClass(opts.icon);
const tone = getModalTone(opts.icon);
activeModal.modal.className = `rh-modal${opts.className ? ` ${opts.className}` : ""}${toneClass ? ` ${toneClass}` : ""}`;
activeModal.closeOnClickOutside = opts.closeOnClickOutside !== false;
actionsEl.innerHTML = "";
const buttons = opts.buttons && Object.keys(opts.buttons).length ? opts.buttons : opts.showCancelButton ? {
confirm: opts.confirmButtonText || "确定",
cancel: opts.cancelButtonText || "取消"
} : { confirm: "确定" };
Object.entries(buttons).forEach(([key, config]) => {
const button = document.createElement("button");
button.type = "button";
button.className = `rh-modal-button ${getButtonRoleClass(key, tone)}`;
button.textContent = typeof config === "string" ? config : config?.text || key;
button.addEventListener("click", () => {
if (key === "cancel") {
closeActiveModal(null);
return;
}
if (key === "confirm") {
closeActiveModal(true);
return;
}
closeActiveModal(key);
});
actionsEl.appendChild(button);
});
}
function showModal(optionsOrTitle, text, icon) {
injectStyles();
const options = normalizeOptions(optionsOrTitle, text, icon);
if (activeModal) {
closeActiveModal(null);
}
return new Promise((resolve) => {
const overlay = document.createElement("div");
overlay.className = "rh-modal-overlay";
const modal = document.createElement("div");
modal.className = "rh-modal";
const iconEl = document.createElement("div");
iconEl.className = "rh-modal-icon";
const titleEl = document.createElement("div");
titleEl.className = "rh-modal-title";
const textEl = document.createElement("div");
textEl.className = "rh-modal-text";
const contentEl = document.createElement("div");
contentEl.className = "rh-modal-content";
const actionsEl = document.createElement("div");
actionsEl.className = "rh-modal-actions";
modal.append(iconEl, titleEl, textEl, contentEl, actionsEl);
overlay.appendChild(modal);
document.body.appendChild(overlay);
const escHandler = (event) => {
if (event.key === "Escape" && activeModal?.closeOnClickOutside) {
closeActiveModal(null);
}
};
overlay.addEventListener("click", (event) => {
if (event.target === overlay && activeModal?.closeOnClickOutside) {
closeActiveModal(null);
}
});
document.addEventListener("keydown", escHandler);
activeModal = {
overlay,
modal,
iconEl,
titleEl,
textEl,
contentEl,
actionsEl,
resolve,
closeOnClickOutside: true,
escHandler
};
renderModal(options);
});
}
function updateOrShowModal(options) {
if (activeModal) {
renderModal(options);
return;
}
void showModal(options);
}
// src/modules/gog/index.ts
var GOG_BUTTON_CLASS = "gog-claim-button";
var GOG_PROCESSED_CLASS = "gog-claimed";
var GOG_CSS = `
.rh-claim-button{
display:inline-flex;align-items:center;gap:0.25em;
padding:0.15em 0.7em;
background:linear-gradient(135deg,#22c55e 0%,#16a34a 100%);
color:#ffffff !important;
font-weight:600;font-size:0.85em;line-height:1.35;
border:none;border-radius:0.35em;
cursor:pointer;text-decoration:none !important;
box-shadow:0 1px 3px rgba(22,163,74,0.35);
transition:all 0.2s ease;
vertical-align:middle;
white-space:nowrap;
margin-left:0.5em;
}
.rh-claim-button:hover{
background:linear-gradient(135deg,#16a34a 0%,#15803d 100%);
box-shadow:0 2px 8px rgba(22,163,74,0.45);
transform:translateY(-1px);
color:#ffffff !important;text-decoration:none !important;
}
.rh-claim-button:active{
transform:translateY(0);
box-shadow:0 1px 2px rgba(22,163,74,0.2);
}
`;
var initialized = false;
var observer = null;
function isEligibleGOGLink(href) {
try {
const url = new URL(href);
return url.hostname === "www.gog.com" && url.pathname === "/giveaway/claim";
} catch {
return false;
}
}
function addButtons() {
for (const link of Array.from(document.querySelectorAll(`a[href*="gog.com/giveaway/claim"]:not(.${GOG_PROCESSED_CLASS})`))) {
link.classList.add(GOG_PROCESSED_CLASS);
const href = link.href;
if (!isEligibleGOGLink(href)) continue;
const button = document.createElement("a");
button.className = `rh-claim-button ${GOG_BUTTON_CLASS}`;
button.href = "javascript:void(0)";
button.target = "_self";
button.textContent = "领取";
button.addEventListener("click", (event) => {
event.preventDefault();
void claimGOGGiveaway("https://www.gog.com/giveaway/claim");
});
link.after(button);
}
}
async function claimGOGGiveaway(url) {
void showModal({
title: "正在领取GOG...",
text: "",
icon: "info"
});
const response = await request({
url,
method: "POST",
data: "{}",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
responseType: "json"
});
if (response.status === 201 || response.status === 409 && response.data?.message === "Already claimed") {
updateOrShowModal({
title: "领取成功!",
text: "",
icon: "success"
});
return true;
}
updateOrShowModal({
title: "领取失败!",
text: response.data?.message || `状态码: ${response.status}`,
icon: "error"
});
return false;
}
function initGOG() {
if (initialized) return;
initialized = true;
GM_addStyle(GOG_CSS);
observer = mountObserver(addButtons);
}
// src/shared/dom.ts
function isHost(host) {
const hosts = Array.isArray(host) ? host : [host];
const currentHost = window.location.hostname;
return hosts.some((candidate) => currentHost === candidate || currentHost.endsWith(`.${candidate}`));
}
// src/modules/ig/addToLib.ts
function updateModal(options) {
updateOrShowModal(options);
}
function parseAddToLibraryRequest(pageHtml, href) {
const pageId = pageHtml.match(/dataToSend\.(gala_page_)?id[\s]*?=[\s]*?'(.*?)';/)?.[2];
if (!pageId) return null;
const csrfToken = pageHtml.match(/<input name="csrfmiddlewaretoken".+?value="(.+?)"/)?.[1];
if (!csrfToken) return null;
const targetUrl = new URL(href);
const gameSlug = targetUrl.pathname.replace(/\//g, "");
const subdomain = targetUrl.hostname.replace(".indiegala.com", "");
const url = new URL(`/developers/ajax/add-to-library/${pageId}/${gameSlug}/${subdomain}`, targetUrl).href;
return { url, csrfToken };
}
function syncOwnedIndieGalaLinks() {
const syncIgLib = window.syncIgLib;
if (typeof syncIgLib !== "function") return;
void syncIgLib(false, false).then((allGames) => {
for (const link of Array.from(document.querySelectorAll('a[href*=".indiegala.com/"]'))) {
link.classList.add("ig-checked");
try {
const href = link.href;
const url = new URL(href);
if (/^https?:\/\/[\w\d]+?\.indiegala\.com\/.+$/.test(href) && allGames.includes(url.pathname.replace(/\//g, ""))) {
link.classList.add("ig-owned");
}
} catch (error) {
console.error(error);
}
}
});
}
async function promptLogin() {
const result = await showModal({
title: "请先登录!",
icon: "error",
showCancelButton: true,
confirmButtonText: "登录",
cancelButtonText: "关闭"
});
if (result) {
window.open("https://www.indiegala.com/login", "_blank");
}
}
async function addToIndiegalaLibrary(target) {
const href = target;
void showModal({
title: "正在获取入库链接...",
text: href,
icon: "info"
});
const pageResponse = await request({
url: href,
method: "GET",
anonymous: false,
timeout: 3e4
});
if (!pageResponse.text) {
console.error(pageResponse);
updateModal({
title: "获取入库链接失败!",
text: href,
icon: "error"
});
return null;
}
if (pageResponse.text.includes("loginRedirect")) {
updateModal({
title: "请先登录!",
text: "https://www.indiegala.com/login",
icon: "error"
});
return null;
}
const addRequest = parseAddToLibraryRequest(pageResponse.text, href);
if (!addRequest) {
console.error(pageResponse);
updateModal({
title: "获取入库Id失败!",
text: href,
icon: "error"
});
return null;
}
updateModal({
title: "正在入库...",
text: href,
icon: "info"
});
const addResponse = await request({
url: addRequest.url,
method: "POST",
responseType: "json",
nocache: true,
headers: {
"content-type": "application/json",
"X-CSRFToken": addRequest.csrfToken,
"X-CSRF-Token": addRequest.csrfToken
},
timeout: 3e4
});
if (addResponse.data?.status === "ok") {
updateModal({
title: "入库成功!",
text: href,
icon: "success"
});
syncOwnedIndieGalaLinks();
return true;
}
if (addResponse.data?.status === "added") {
updateModal({
title: "已在库中!",
text: href,
icon: "warning"
});
return true;
}
if (addResponse.data?.status === "login" || addResponse.data?.status === "auth") {
await promptLogin();
return false;
}
console.error(addResponse);
updateModal({
title: "入库失败!",
text: href,
icon: "error"
});
return null;
}
// src/modules/ig/index.ts
var IG_BUTTON_CLASS = "add-to-library";
var IG_PROCESSED_CLASS = "ig-add2lib";
var IG_CSS = `.${IG_BUTTON_CLASS}{margin-left:10px;}`;
var initialized2 = false;
var observer2 = null;
function isEligibleIndieGalaLink(href) {
try {
const url = new URL(href);
return /^https?:$/.test(url.protocol) && /^.+?\.indiegala\.com$/.test(url.hostname) && !["/login", "/library"].includes(url.pathname) && url.pathname !== "/";
} catch {
return false;
}
}
function addButtons2() {
for (const link of Array.from(document.querySelectorAll(`a[href*=".indiegala.com/"]:not(.${IG_PROCESSED_CLASS})`))) {
link.classList.add(IG_PROCESSED_CLASS);
const href = link.href;
if (!isEligibleIndieGalaLink(href)) continue;
const button = document.createElement("a");
button.className = IG_BUTTON_CLASS;
button.href = "javascript:void(0)";
button.target = "_self";
button.dataset.href = href;
button.textContent = "入库";
button.addEventListener("click", (event) => {
event.preventDefault();
void addToIndiegalaLibrary(href);
});
link.after(button);
}
}
function collectBatchLinks() {
const links = Array.from(document.querySelectorAll(`a.${IG_BUTTON_CLASS}`)).filter((button) => !button.previousElementSibling?.classList.contains("ig-owned")).map((button) => button.dataset.href || "").filter(Boolean);
return [...new Set(links)];
}
function initIG() {
if (initialized2 || isHost("indiegala.com")) return;
initialized2 = true;
GM_addStyle(IG_CSS);
observer2 = mountObserver(addButtons2);
}
async function runIGBatch() {
if (isHost("indiegala.com")) return;
addButtons2();
const links = collectBatchLinks();
const failedLinks = [];
for (const link of links) {
const result = await addToIndiegalaLibrary(link);
if (result === false) break;
if (!result) {
failedLinks.push(link);
}
}
if (failedLinks.length === 0) {
void showModal({
title: "全部任务完成!",
icon: "success"
});
return;
}
void showModal({
titleText: "以下任务未完成!",
icon: "warning",
text: failedLinks.join("\n")
});
}
// src/shared/storage.ts
var SETTINGS_KEY = "setting";
var defaultSettings = {
steam: {
newTab: false,
copyListen: true,
selectListen: true,
clickListen: true,
allKeyListen: false,
asf: false,
asfProtocol: "http",
asfHost: "127.0.0.1",
asfPort: 1242,
asfPassword: "",
asfBot: ""
},
ig: {
enableButtons: true
},
itch: {
autoClose: true
},
gog: {
enableButtons: true
}
};
function mergeSettings(settings = {}, base = defaultSettings) {
return {
steam: {
...base.steam,
...settings.steam
},
ig: {
...base.ig,
...settings.ig
},
itch: {
...base.itch,
...settings.itch
},
gog: {
...base.gog,
...settings.gog
}
};
}
function getSettings() {
return mergeSettings(GM_getValue(SETTINGS_KEY, {}));
}
function setSettings(settings) {
GM_setValue(SETTINGS_KEY, mergeSettings(settings, getSettings()));
}
// src/modules/itch/redeem.ts
var GAME_URL_RE = /^https?:\/\/.+?\.itch\.io\/[^/?#]+\/?(?:purchase(?:\?.*)?)?$/i;
var REWARD_PURCHASE_URL_RE = /^https?:\/\/.+?\.itch\.io\/[^/?#]+\/purchase\?[^#]*reward_id=/i;
var BUNDLE_URL_RE = /^https?:\/\/itch\.io\/s\/\d+\/.+/i;
function log(message, icon = "info", details) {
if (typeof message !== "string") {
console.log(message);
return;
}
updateOrShowModal({
title: message,
text: details,
icon,
className: "break-all"
});
console.log(details ? `${message}
${details}` : message);
}
function parseHtml(html) {
return new DOMParser().parseFromString(html, "text/html");
}
function textContent(documentOrElement, selector) {
return documentOrElement.querySelector(selector)?.textContent?.trim() || "";
}
function inputValue(document2, selector) {
const element = document2.querySelector(selector);
return element?.value || element?.getAttribute("value") || "";
}
function isFreePurchasePage(document2) {
const buttonMessage = document2.querySelector(".button_message");
const dollars = buttonMessage?.querySelector(".dollars[itemprop]")?.textContent || "";
const buyMessage = buttonMessage?.querySelector(".buy_message")?.textContent || "";
const placeholder = document2.querySelector(".money_input")?.placeholder || "";
return /0\.00/i.test(dollars) || /0\.00/i.test(placeholder) || /自己出价|Name your own price/i.test(buyMessage);
}
function isOwnedPageText(html) {
return html.includes("purchase_banner_inner");
}
function isLinkedDownloadPage(document2) {
const innerText = textContent(document2, "div.inner_column");
return /This page is linked|此页面已链接到帐户/i.test(innerText) || document2.querySelector("a.button.download_btn[data-upload_id]") !== null;
}
function normalizeGameUrl(target) {
let url;
try {
url = new URL(target, window.location.href);
} catch {
return null;
}
if (REWARD_PURCHASE_URL_RE.test(url.href)) return url.href;
if (!GAME_URL_RE.test(url.href)) return null;
if (url.pathname.endsWith("/purchase")) {
url.pathname = url.pathname.replace(/\/purchase\/?$/, "");
url.search = "";
}
url.hash = "";
return url.href.replace(/\/$/, "");
}
async function reportRequestFailure(message, response) {
log(message, "error");
log(response);
}
async function checkOwnedAndRedeem(url) {
log("当前游戏链接:", "info", url);
log("正在检测游戏是否拥有...", "info", url);
const response = await request({
url,
method: "GET"
});
if (!response.ok || !response.text) {
await reportRequestFailure("请求失败!", response);
return;
}
if (isOwnedPageText(response.text)) {
log("游戏已拥有!", "success");
return;
}
await purchase(url);
}
async function purchase(url) {
try {
log("正在加载购买页面...", "info", url);
const purchaseUrl = url.includes("/purchase") ? url : `${url}/purchase`;
const response = await request({
url: purchaseUrl,
method: "GET"
});
if (!response.ok || !response.text) {
await reportRequestFailure("请求失败!", response);
return;
}
const document2 = parseHtml(response.text);
if (!isFreePurchasePage(document2)) {
log("价格不为 0, 可能活动已结束!", "error");
return;
}
const csrfToken = inputValue(document2, '[name="csrf_token"]');
const rewardId = inputValue(document2, '[name="reward_id"]');
if (!csrfToken) {
log("获取 csrf_token 失败!", "error");
return;
}
await download(purchaseUrl.replace(/\/purchase.*/, ""), csrfToken, rewardId);
} catch (error) {
log("请求失败!", "error");
log(error);
}
}
async function download(url, csrfToken, rewardId) {
log("正在请求下载页面...", "info", url);
const body = new URLSearchParams({ csrf_token: csrfToken });
if (rewardId) body.set("reward_id", rewardId);
const response = await request({
url: `${url}/download_url`,
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
},
data: body.toString(),
responseType: "json"
});
if (response.ok && response.data?.url) {
await loadDownload(response.data.url, url);
return;
}
await reportRequestFailure("请求失败!", response);
}
function downloadHeaders(url, referer) {
return {
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
DNT: "1",
Host: url.hostname,
Referer: referer,
"Upgrade-Insecure-Requests": "1"
};
}
async function loadDownload(downloadUrl, referer) {
log("正在加载下载页面...");
const url = new URL(downloadUrl);
const response = await request({
url: url.href,
method: "GET",
headers: downloadHeaders(url, referer)
});
if (!response.ok || !response.text) {
await reportRequestFailure("请求失败!", response);
return;
}
const document2 = parseHtml(response.text);
const claimButton = Array.from(document2.querySelectorAll("button.button")).find((button) => /link|claim|链接/i.test(button.textContent || ""));
const claimForm = document2.querySelector('form[action*="claim-key"]') || claimButton?.closest("form");
if (isLinkedDownloadPage(document2)) {
log("领取成功!", "success");
} else if (claimForm) {
const action = claimForm.getAttribute("action");
const csrfToken = claimForm.querySelector('input[name="csrf_token"]')?.value || "";
if (action && csrfToken) {
await claimGame(new URL(action, url.href).href, csrfToken, url.href);
} else {
log("获取领取表单失败!", "error");
}
} else if (response.response?.finalUrl?.includes("/register")) {
log("领取失败,请先登录!", "error");
} else {
log("领取完成,结果未知!", "success");
}
const checker = window.checkItchGame;
if (typeof checker === "function") checker();
}
async function claimGame(action, token, referer) {
log("正在领取游戏...");
const url = new URL(action);
const response = await request({
url: url.href,
method: "POST",
headers: {
...downloadHeaders(url, referer),
"Cache-Control": "max-age=0",
"Content-Type": "application/x-www-form-urlencoded",
Origin: url.origin
},
data: `csrf_token=${encodeURIComponent(token)}`
});
if (response.ok && response.text) {
const document2 = parseHtml(response.text);
log(isLinkedDownloadPage(document2) ? "领取成功!" : "领取完成,结果未知!", "success");
} else if (response.response?.finalUrl?.includes("/register")) {
log("请先登录!", "error");
log(response);
} else {
await reportRequestFailure("请求失败!", response);
}
}
function handleItchDownloadPage() {
for (const button of Array.from(document.querySelectorAll("button.button"))) {
if (/link|claim|链接/i.test(button.textContent || "")) button.click();
}
if (getSettings().itch.autoClose && isLinkedDownloadPage(document)) {
window.close();
}
}
function injectItchPurchaseButton() {
const directDownloadButton = document.querySelector("a.direct_download_btn");
if (/No thanks, just take me to the downloads|不用了,请带我去下载页面/i.test(directDownloadButton?.textContent || "")) {
directDownloadButton?.click();
return;
}
if (document.querySelector(".purchase_banner_inner") || !isFreePurchasePage(document)) return;
const buyButton = document.querySelector(".buy_btn");
if (!buyButton || buyButton.nextElementSibling?.classList.contains("redeem-itch-purchase")) return;
const button = document.createElement("a");
button.href = "javascript:void(0)";
button.target = "_self";
button.className = "button redeem-itch-purchase";
button.title = "仅支持免费游戏";
button.dataset.itchHref = buyButton.href;
button.textContent = "后台领取";
button.addEventListener("click", (event) => {
event.preventDefault();
void redeemItchGame(button.dataset.itchHref || buyButton.href);
});
buyButton.after(button);
}
async function redeemItchGame(target) {
log("当前游戏/优惠包链接:", "info", target);
if (BUNDLE_URL_RE.test(target)) {
await redeemItchBundle(target);
return;
}
const url = normalizeGameUrl(target);
if (!url) return;
await checkOwnedAndRedeem(url);
}
// src/modules/itch/bundle.ts
var BUNDLE_URL_RE2 = /^https?:\/\/itch\.io\/s\/\d+\/.+/i;
function log2(message, icon = "info", details) {
if (typeof message !== "string") {
console.log(message);
return;
}
updateOrShowModal({ title: message, text: details, icon, className: "break-all" });
console.log(details ? `${message}
${details}` : message);
}
function parseBundleGames(html, baseUrl) {
const document2 = new DOMParser().parseFromString(html, "text/html");
const games = Array.from(document2.querySelectorAll(".game_grid_widget.promo_game_grid a.thumb_link.game_link, a.thumb_link.game_link")).map((link) => new URL(link.href || link.getAttribute("href") || "", baseUrl).href.replace(/\/$/, "")).filter((href) => /^https?:\/\/.+?\.itch\.io\/[^/?#]+$/i.test(href));
return [...new Set(games)];
}
async function getItchBundleGames(url) {
log2("正在获取优惠包信息...", "info", url);
const response = await request({
url,
method: "GET"
});
if (!response.ok || !response.text) {
log2("请求失败!", "error");
log2(response);
return [];
}
if (response.text.includes("not_active_notification")) {
log2("活动已结束!", "error");
return [];
}
return parseBundleGames(response.text, url);
}
async function redeemItchBundle(url) {
if (!BUNDLE_URL_RE2.test(url)) return;
const games = await getItchBundleGames(url);
for (const game of games) {
await redeemItchGame(game);
}
}
async function redeemCurrentItchBundle() {
const games = Array.from(document.querySelectorAll(".thumb_link.game_link"));
for (const game of games) {
await redeemItchGame(game.href);
}
}
// src/modules/itch/extract.ts
var GAME_LINK_RE = /^https?:\/\/.+?\.itch\.io\/[^/?#]+\/?(?:purchase)?$/i;
var REWARD_LINK_RE = /^https?:\/\/.+?\.itch\.io\/[^/?#]+\/purchase\?[^#]*reward_id=/i;
var BUNDLE_LINK_RE = /^https?:\/\/itch\.io\/s\/\d+\/.+/i;
function log3(message, icon = "info", details) {
updateOrShowModal({ title: message, text: details, icon, className: "break-all" });
console.log(details ? `${message}
${details}` : message);
}
function normalizeHref(href) {
try {
const url = new URL(href, window.location.href);
url.hash = "";
if (BUNDLE_LINK_RE.test(url.href) || REWARD_LINK_RE.test(url.href)) return url.href.replace(/\/$/, "");
if (!GAME_LINK_RE.test(url.href)) return null;
if (url.pathname.endsWith("/purchase")) {
url.pathname = url.pathname.replace(/\/purchase\/?$/, "");
url.search = "";
}
return url.href.replace(/\/$/, "");
} catch {
return null;
}
}
async function expandItchLink(href) {
if (BUNDLE_LINK_RE.test(href)) {
return getItchBundleGames(href);
}
const normalized = normalizeHref(href);
return normalized ? [normalized] : [];
}
async function extractAndRedeemItchLinks() {
log3("正在提取链接,请稍候...");
const links = Array.from(document.querySelectorAll('a[href*="itch.io"]')).filter((link) => !link.classList.contains("itch-io-game-link-owned")).filter((link) => !/itch\.io\/(?:b|c)\//i.test(link.href)).map((link) => link.dataset.itchHref || link.href);
const games = [];
for (const link of links) {
log3("正在处理游戏/优惠包链接:", "info", link);
games.push(...await expandItchLink(link));
}
for (const game of [...new Set(games)]) {
await redeemItchGame(game);
}
log3("全部领取完成!", "success");
}
// src/modules/itch/index.ts
var ITCH_PROCESSED_CLASS = "redeem-itch-game";
var EXTERNAL_HOSTS = [
"keylol.com",
"www.steamgifts.com",
"www.reddit.com",
"new.isthereanydeal.com",
"freegames.codes",
"itchclaim.tmbpeter.com",
"shaigrorb.github.io"
];
var ITCH_CSS = `
.rh-modal.break-all .rh-modal-title{word-wrap:break-word;word-break:break-all;}
.rh-claim-button{
display:inline-flex;align-items:center;gap:0.25em;
padding:0.15em 0.7em;
background:linear-gradient(135deg,#22c55e 0%,#16a34a 100%);
color:#ffffff !important;
font-weight:600;font-size:0.85em;line-height:1.35;
border:none;border-radius:0.35em;
cursor:pointer;text-decoration:none !important;
box-shadow:0 1px 3px rgba(22,163,74,0.35);
transition:all 0.2s ease;
vertical-align:middle;
white-space:nowrap;
margin-left:0.5em;
}
.rh-claim-button:hover{
background:linear-gradient(135deg,#16a34a 0%,#15803d 100%);
box-shadow:0 2px 8px rgba(22,163,74,0.45);
transform:translateY(-1px);
color:#ffffff !important;text-decoration:none !important;
}
.rh-claim-button:active{
transform:translateY(0);
box-shadow:0 1px 2px rgba(22,163,74,0.2);
}
.freegames-codes .rh-claim-button{margin-top:0.5em !important;margin-left:0 !important;}
.shaigrorb-itch-button{position:relative;height:min-content;right:39px;background-color:#16a34a;top:4px;text-decoration-line:none;color:white;font-weight:bold;border-radius:2px;padding:5px;font-size:13px;}
`;
var initialized3 = false;
var observer3 = null;
function isDownloadPage(url) {
return /^https?:\/\/.+\.itch\.io\/[\w-]+\/download(?:\/.*|\?.*)?$/i.test(url);
}
function isPurchasePage(url) {
return /^https?:\/\/.*?itch\.io\/.*?\/purchase(?:\?.*)?$/i.test(url);
}
function isBundlePage(url) {
return /^https?:\/\/itch\.io\/s\/\d+\/.+/i.test(url);
}
function isEligibleItchHref(href) {
try {
const url = new URL(href, window.location.href);
return /(^|\.)itch\.io$/i.test(url.hostname) && !/itch\.io\/(?:b|c)\//i.test(url.href) && (/^https?:\/\/itch\.io\/s\/\d+\/.+/i.test(url.href) || /^https?:\/\/.+?\.itch\.io\/[^/?#]+\/?(?:purchase(?:\?.*)?)?$/i.test(url.href));
} catch {
return false;
}
}
function createRedeemButton(href) {
const button = document.createElement("a");
button.href = "javascript:void(0);";
button.target = "_self";
button.dataset.itchHref = href;
button.textContent = "领取";
button.addEventListener("click", (event) => {
event.preventDefault();
void redeemItchGame(href);
});
if (window.location.hostname === "freegames.codes") {
button.className = "details__buy rh-claim-button";
} else if (window.location.hostname === "shaigrorb.github.io") {
button.className = "shaigrorb-itch-button rh-claim-button";
} else {
button.className = "rh-claim-button";
}
return button;
}
function addExternalRedeemButtons() {
for (const link of Array.from(document.querySelectorAll(`a[href*="itch.io"]:not(.${ITCH_PROCESSED_CLASS})`))) {
link.classList.add(ITCH_PROCESSED_CLASS);
const href = link.href;
if (!isEligibleItchHref(href)) continue;
const button = createRedeemButton(href);
if (window.location.hostname === "shaigrorb.github.io") {
const card = link.closest(".item-card");
(card || link).after(button);
} else {
link.after(button);
}
}
}
function injectBundleButton() {
if (document.querySelector("#redeem-itch-io")) return;
const button = document.createElement("button");
button.id = "redeem-itch-io";
button.className = "button";
button.textContent = "后台领取";
button.addEventListener("click", () => {
void redeemCurrentItchBundle();
});
const buyRowButton = document.querySelector(".promotion_buy_row .buy_game_btn");
if (buyRowButton) {
button.setAttribute("style", "font-size:18px;letter-spacing:0.025em;line-height:36px;height:40px;padding:0 20px;margin:0 16px");
buyRowButton.after(button);
return;
}
const countdownRow = document.querySelector(".countdown_row");
if (!countdownRow) return;
const wrapper = document.createElement("div");
wrapper.style.width = "100%";
button.setAttribute("style", "font-size:18px;letter-spacing:0.025em;line-height:36px;padding:0 20px;margin:10px 30%;width:40%;");
wrapper.append(button);
countdownRow.prepend(wrapper);
}
function initItchHostPage() {
const url = window.location.href;
if (isDownloadPage(url)) {
handleItchDownloadPage();
return;
}
if (isPurchasePage(url)) {
injectItchPurchaseButton();
return;
}
if (isBundlePage(url)) {
injectBundleButton();
}
}
function initItch() {
if (initialized3) return;
initialized3 = true;
GM_addStyle(ITCH_CSS);
if (isHost("itch.io")) {
initItchHostPage();
return;
}
if (!isHost(EXTERNAL_HOSTS)) return;
document.documentElement.classList.toggle("freegames-codes", window.location.hostname === "freegames.codes");
observer3 = mountObserver(addExternalRedeemButtons);
}
async function runItchExtract() {
await extractAndRedeemItchLinks();
}
// src/shared/regex.ts
var STEAM_KEY_RE = /\b(?:[A-Z0-9]{5}-){2,4}[A-Z0-9]{5}\b/gi;
function extractSteamKeys(input) {
const matches = input.match(STEAM_KEY_RE) ?? [];
return [...new Set(matches.map((key) => key.toUpperCase()))];
}
// src/modules/steam/settings.ts
function getSteamSettings() {
const raw = GM_getValue("setting", {});
if (raw && "steam" in raw) {
return getSettings().steam;
}
if (raw && Object.keys(raw).some((key) => key in defaultSettings.steam)) {
const migrated = { ...defaultSettings.steam, ...raw };
if (typeof migrated.asfPort === "string") {
migrated.asfPort = Number.parseInt(migrated.asfPort, 10) || defaultSettings.steam.asfPort;
}
setSettings({ steam: migrated });
return migrated;
}
return getSettings().steam;
}
function saveSteamSettings(settings) {
const next = {
...getSteamSettings(),
...settings,
asfPort: Number(settings.asfPort ?? getSteamSettings().asfPort) || defaultSettings.steam.asfPort
};
setSettings({ steam: next });
}
function showHistory() {
const history = GM_getValue("history");
if (Array.isArray(history)) {
showModal({
closeOnClickOutside: false,
className: "swal-user",
title: "上次激活记录:",
content: htmlToElement(history[0]),
buttons: { confirm: "确定" }
});
if (history[1]) {
setTimeout(() => {
const textarea = document.querySelector(".rh-modal-content textarea");
if (textarea) textarea.value = history[1] ?? "";
}, 0);
}
} else {
showModal({ closeOnClickOutside: false, title: "没有操作记录!", icon: "error", buttons: { cancel: "关闭" } });
}
}
function showSwitchKey() {
const content = htmlToElement(`
<div class="switch-key">
<div class="switch-key-left"><p>key</p><p>key</p><p>key</p><input name="keyType" type="radio" value="1"/></div>
<div class="switch-key-right"><p> </p><p>key,key,key</p><p> </p><input name="keyType" type="radio" value="2"/></div>
</div>
`);
showModal({
closeOnClickOutside: false,
title: "请选择要转换成什么格式:",
content,
buttons: { confirm: "确定", cancel: "关闭" }
}).then((value) => {
if (value) {
const selectedValue = content.querySelector('input[name="keyType"]:checked')?.value;
if (selectedValue) {
showSwitchArea(selectedValue);
} else {
showModal({ closeOnClickOutside: false, title: "请选择要将key转换成什么格式!", icon: "warning" }).then(() => showSwitchKey());
}
}
});
content.querySelectorAll("div").forEach((div) => div.addEventListener("click", () => div.querySelector("input")?.click()));
}
function showSwitchArea(type) {
const textarea = document.createElement("textarea");
textarea.style.width = "80%";
textarea.style.height = "100px";
showModal({
closeOnClickOutside: false,
title: "请输入要转换的key:",
content: textarea,
buttons: { confirm: "转换", back: "返回", cancel: "关闭" }
}).then((value) => {
if (value === "back") {
showSwitchKey();
} else if (value) {
switchKey(textarea.value, type);
}
});
}
function switchKey(key, type) {
const keys = extractSteamKeys(key);
if (type === "1") {
showKey(keys.join("\n"), type);
} else if (type === "2") {
showKey(keys.join(","), type);
}
}
function showKey(key, type) {
const textarea = document.createElement("textarea");
textarea.style.width = "80%";
textarea.style.height = "100px";
textarea.readOnly = true;
textarea.value = key;
textarea.addEventListener("click", () => textarea.select());
showModal({
closeOnClickOutside: false,
icon: "success",
title: "转换成功!",
content: textarea,
buttons: { confirm: "返回", cancel: "关闭" }
}).then((value) => {
if (value) showSwitchArea(type);
});
}
function htmlToElement(html) {
const template = document.createElement("template");
template.innerHTML = html.trim();
return template.content.firstElementChild ?? document.createElement("div");
}
function openSteamSettingsDialog() {
const setting = getSteamSettings();
const div = htmlToElement(`
<div id="hclonely-asf">
<input type="checkbox" name="newTab" ${setting.newTab ? "checked" : ""} title="开启ASF激活后此功能无效"/>
<span title="开启ASF激活后此功能无效">新标签页激活</span><br/>
<input type="checkbox" name="copyListen" ${setting.copyListen ? "checked" : ""} title="复制key时询问是否激活"/>
<span title="复制key时询问是否激活">开启复制捕捉</span>
<input type="checkbox" name="selectListen" ${setting.selectListen ? "checked" : ""} title="选中key时显示激活图标"/>
<span title="选中key时显示激活图标">开启选中捕捉</span>
<input type="checkbox" name="clickListen" ${setting.clickListen ? "checked" : ""} title="点击key时添加激活链接"/>
<span title="点击key时添加激活链接">开启点击捕捉</span><br/>
<input type="checkbox" name="allKeyListen" ${setting.allKeyListen ? "checked" : ""} title="匹配页面内所有符合steam key格式的内容"/>
<span title="匹配页面内所有符合steam key格式的内容">捕捉页面内所有key</span>
<div class="rh-modal-title">ASF IPC设置</div>
<span>ASF IPC协议</span><input type="text" name="asfProtocol" value="${setting.asfProtocol}" placeholder="http或https,默认为http"/><br/>
<span>ASF IPC地址</span><input type="text" name="asfHost" value="${setting.asfHost}" placeholder="ip地址或域名,默认为127.0.0.1"/><br/>
<span>ASF IPC端口</span><input type="text" name="asfPort" value="${setting.asfPort}" placeholder="默认1242"/><br/>
<span>ASF IPC密码</span><input type="text" name="asfPassword" value="${setting.asfPassword}" placeholder="ASF IPC密码"/><br/>
<span>ASF Bot名字</span><input type="text" name="asfBot" value="${setting.asfBot}" placeholder="ASF Bot name,可留空"/><br/>
<input type="checkbox" name="asf" ${setting.asf ? "checked" : ""} title="此功能默认关闭新标签页激活"/>
<span title="此功能默认关闭新标签页激活">开启ASF激活</span>
</div>
`);
showModal({
closeOnClickOutside: false,
className: "asf-class",
title: "全局设置",
content: div,
buttons: { save: "保存", showHistory: "上次激活记录", showSwitchKey: "Key格式转换", cancel: "取消" }
}).then((value) => {
if (value === "save") {
const next = {};
div.querySelectorAll("input").forEach((input) => {
const name = input.name;
if (!name) return;
if (input.type === "checkbox") {
next[name] = input.checked;
} else if (name === "asfPort") {
next.asfPort = Number.parseInt(input.value, 10) || defaultSettings.steam.asfPort;
} else {
next[name] = input.value;
}
});
saveSteamSettings(next);
showModal({ closeOnClickOutside: false, icon: "success", title: "保存成功!", text: "刷新页面后生效!", buttons: { confirm: "确定" } });
} else if (value === "showHistory") {
showHistory();
} else if (value === "showSwitchKey") {
showSwitchKey();
}
});
}
// src/modules/steam/asfCommands.generated.ts
var ASF_COMMANDS_HTML = `<table role="table" class="hclonely">
<thead>
<tr>
<th>命令</th>
<th>权限</th>
<th>描述</th>
<th>操作</th></tr>
</thead>
<tbody>
<tr>
<td><code>2fa [Bots]</code></td>
<td><code>Master</code></td>
<td>为指定机器人生成临时的<strong><a href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Two-factor-authentication-zh-CN">两步验证</a></strong>令牌。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="2fa [Bots]">使用</button></td></tr>
<tr>
<td><code>2fafinalize [Bots] <ActivationCode></code></td>
<td><code>Master</code></td>
<td>使用短信或邮件验证码,完成为指定机器人绑定新<a href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Two-factor-authentication-zh-CN#%E5%88%9B%E5%BB%BA"><strong>两步验证</strong></a>凭据的流程。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="2fafinalize [Bots] <ActivationCode>">使用</button></td></tr>
<tr>
<td><code>2fafinalized [Bots] <2FACodeFromApp></code></td>
<td><code>Master</code></td>
<td>为指定机器人导入已创建完成的<a href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Two-factor-authentication-zh-CN#%E5%88%9B%E5%BB%BA"><strong>两步验证</strong></a>凭据,并用两步验证令牌代码验证。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="2fafinalized [Bots] <2FACodeFromApp>">使用</button></td></tr>
<tr>
<td><code>2fafinalizedforce [Bots]</code></td>
<td><code>Master</code></td>
<td>为指定机器人导入已创建完成的<a href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Two-factor-authentication-zh-CN#%E5%88%9B%E5%BB%BA"><strong>两步验证</strong></a>凭据,并跳过两步验证令牌代码验证。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="2fafinalizedforce [Bots]">使用</button></td></tr>
<tr>
<td><code>2fainit [Bots]</code></td>
<td><code>Master</code></td>
<td>开始为指定机器人绑定新<a href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Two-factor-authentication-zh-CN#%E5%88%9B%E5%BB%BA"><strong>两步验证</strong></a>凭据的流程。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="2fainit [Bots]">使用</button></td></tr>
<tr>
<td><code>2fano [Bots]</code></td>
<td><code>Master</code></td>
<td>为指定机器人拒绝所有等待操作的<a href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Two-factor-authentication-zh-CN"><strong>两步验证</strong></a>交易确认。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="2fano [Bots]">使用</button></td></tr>
<tr>
<td><code>2faok [Bots]</code></td>
<td><code>Master</code></td>
<td>为指定机器人接受所有等待操作的<a href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Two-factor-authentication-zh-CN"><strong>两步验证</strong></a>交易确认。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="2faok [Bots]">使用</button></td></tr>
<tr>
<td><code>addlicense [Bots] <Licenses></code></td>
<td><code>Operator</code></td>
<td>为指定机器人激活给定的 <code>Licenses </code>(许可),该参数解释详见<a href="#%E8%AE%B8%E5%8F%AF"><strong>下文</strong></a>(仅限免费游戏)。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="addlicense [Bots] <Licenses>">使用</button></td></tr>
<tr>
<td><code>balance [Bots]</code></td>
<td><code>Master</code></td>
<td>显示指定机器人的 Steam 钱包余额。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="balance [Bots]">使用</button></td></tr>
<tr>
<td><code>bgr [Bots]</code></td>
<td><code>Master</code></td>
<td>显示指定机器人的 <strong><a href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Background-games-redeemer-zh-CN">BGR</a></strong>(后台游戏激活器)队列信息。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="bgr [Bots]">使用</button></td></tr>
<tr>
<td><code>bgrclear [Bots]</code></td>
<td><code>Master</code></td>
<td>清除指定机器人的<a href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Background-games-redeemer-zh-CN"><strong>后台游戏激活器</strong></a>队列。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="bgrclear [Bots]">使用</button></td></tr>
<tr>
<td><code>encrypt <encryptionMethod> <stringToEncrypt></code></td>
<td><code>Owner</code></td>
<td>以给定的加密方式加密字符串——详见<a href="#encrypt-%E5%91%BD%E4%BB%A4"><strong>下文的解释</strong></a>。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="encrypt <encryptionMethod> <stringToEncrypt>">使用</button></td></tr>
<tr>
<td><code>exit</code></td>
<td><code>Owner</code></td>
<td>完全停止 ASF 进程。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="exit">使用</button></td></tr>
<tr>
<td><code>farm [Bots]</code></td>
<td><code>Master</code></td>
<td>重新启动指定机器人的挂卡模块。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="farm [Bots]">使用</button></td></tr>
<tr>
<td><code>fb [Bots]</code></td>
<td><code>Master</code></td>
<td>列出指定机器人的自动挂卡黑名单。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="fb [Bots]">使用</button></td></tr>
<tr>
<td><code>fbadd [Bots] <AppIDs></code></td>
<td><code>Master</code></td>
<td>将给定的 <code>AppIDs</code> 加入指定机器人的自动挂卡黑名单。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="fbadd [Bots] <AppIDs>">使用</button></td></tr>
<tr>
<td><code>fbrm [Bots] <AppIDs></code></td>
<td><code>Master</code></td>
<td>将给定的 <code>AppIDs</code>(或使用 <code>all</code> 表示所有)从指定机器人的自动挂卡黑名单中移除。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="fbrm [Bots] <AppIDs>">使用</button></td></tr>
<tr>
<td><code>fq [Bots]</code></td>
<td><code>Master</code></td>
<td>列出指定机器人的优先挂卡队列。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="fq [Bots]">使用</button></td></tr>
<tr>
<td><code>fqadd [Bots] <AppIDs></code></td>
<td><code>Master</code></td>
<td>将给定的 <code>AppIDs</code> 加入指定机器人的优先挂卡队列。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="fqadd [Bots] <AppIDs>">使用</button></td></tr>
<tr>
<td><code>fqrm [Bots] <AppIDs></code></td>
<td><code>Master</code></td>
<td>将给定的 <code>AppIDs</code>(或使用 <code>all</code> 表示所有)从指定机器人的优先挂卡队列中移除。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="fqrm [Bots] <AppIDs>">使用</button></td></tr>
<tr>
<td><code>hash <hashingMethod> <stringToHash></code></td>
<td><code>Owner</code></td>
<td>以指定的加密方式生成给定字符串的哈希值——详见<a href="#hash-%E5%91%BD%E4%BB%A4"><strong>下文的解释</strong></a>。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="hash <hashingMethod> <stringToHash>">使用</button></td></tr>
<tr>
<td><code>help</code></td>
<td><code>FamilySharing</code></td>
<td>显示帮助(指向此页面的链接)。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="help">使用</button></td></tr>
<tr>
<td><code>input [Bots] <Type> <Value></code></td>
<td><code>Master</code></td>
<td>为指定机器人填写给定的输入值,仅在 <code>Headless</code> 模式中可用——详见<a href="#input-%E5%91%BD%E4%BB%A4"><strong>下文的解释</strong></a>。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="input [Bots] <Type> <Value>">使用</button></td></tr>
<tr>
<td><code>inventory [Bots]</code></td>
<td><code>Operator</code></td>
<td>显示指定机器人的库存摘要。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="inventory [Bots]">使用</button></td></tr>
<tr>
<td><code>level [Bots]</code></td>
<td><code>Master</code></td>
<td>显示指定机器人的 Steam 帐户等级。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="level [Bots]">使用</button></td></tr>
<tr>
<td><code>loot [Bots]</code></td>
<td><code>Master</code></td>
<td>将指定机器人的所有 <code>LootableTypes</code> 社区物品拾取到其 <code>SteamUserPermissions</code> 属性中设置的 <code>Master</code> 用户(如果有多个则取 steamID 最小的)。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="loot [Bots]">使用</button></td></tr>
<tr>
<td><code>loot@ [Bots] <AppIDs></code></td>
<td><code>Master</code></td>
<td>将指定机器人的所有符合给定 <code>AppIDs</code> 的 <code>LootableTypes</code> 社区物品拾取到其 <code>SteamUserPermissions</code> 属性中设置的 <code>Master</code> 用户(如果有多个则取 steamID 最小的)。 此命令与 <code>loot%</code> 相反。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="loot@ [Bots] <AppIDs>">使用</button></td></tr>
<tr>
<td><code>loot% [Bots] <AppIDs></code></td>
<td><code>Master</code></td>
<td>将指定机器人的所有不符合给定 <code>AppIDs</code> 的 <code>LootableTypes</code> 社区物品拾取到其 <code>SteamUserPermissions</code> 属性中设置的 <code>Master</code> 用户(如果有多个则取 steamID 最小的)。 此命令与 <code>loot@</code> 相反。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="loot% [Bots] <AppIDs>">使用</button></td></tr>
<tr>
<td><code>loot^ [Bots] <AppID> <ContextID></code></td>
<td><code>Master</code></td>
<td>将指定机器人的 <code>ContextID</code> 库存分类中符合给定 <code>AppID</code> 的物品拾取到其 <code>SteamUserPermissions</code> 属性中设置的 <code>Master</code> 用户(如果有多个则取 steamID 最小的)。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="loot^ [Bots] <AppID> <ContextID>">使用</button></td></tr>
<tr>
<td><code>loot& [Bots] <AppID> <ContextID> <Rarities></code></td>
<td><code>Master</code></td>
<td>将指定机器人的 <code>ContextID</code> 库存分类中符合给定 <code>AppID</code> 并且符合给定 <strong><a href="#%E5%B7%B2%E7%9F%A5%E7%9A%84%E7%A8%80%E6%9C%89%E5%BA%A6"><code>Rarities</code></a></strong> 的物品拾取到其 <code>SteamUserPermissions</code> 属性中设置的 <code>Master</code> 用户(如果有多个则取 steamID 最小的)。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="loot& [Bots] <AppID> <ContextID> <Rarities>">使用</button></td></tr>
<tr>
<td><code>mab [Bots]</code></td>
<td><code>Master</code></td>
<td>列出 <strong><a href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/ItemsMatcherPlugin-zh-CN#matchactively%E4%B8%BB%E5%8A%A8%E5%8C%B9%E9%85%8D"><code>MatchActively</code></a></strong> 自动交易的 App 黑名单。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="mab [Bots]">使用</button></td></tr>
<tr>
<td><code>mabadd [Bots] <AppIDs></code></td>
<td><code>Master</code></td>
<td>将给定的 <code>AppIDs</code> 加入到 <strong><a href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/ItemsMatcherPlugin-zh-CN#matchactively%E4%B8%BB%E5%8A%A8%E5%8C%B9%E9%85%8D"><code>MatchActively</code></a></strong> 自动交易的 App 黑名单。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="mabadd [Bots] <AppIDs>">使用</button></td></tr>
<tr>
<td><code>mabrm [Bots] <AppIDs></code></td>
<td><code>Master</code></td>
<td>将给定的 <code>AppIDs</code>(或使用 <code>all</code> 表示所有)从 <strong><a href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/ItemsMatcherPlugin-zh-CN#matchactively%E4%B8%BB%E5%8A%A8%E5%8C%B9%E9%85%8D"><code>MatchActively</code></a></strong> 自动交易的 App 黑名单中移除。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="mabrm [Bots] <AppIDs>">使用</button></td></tr>
<tr>
<td><code>match [Bots]</code></td>
<td><code>Master</code></td>
<td>控制 <strong><a href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/ItemsMatcherPlugin-zh-CN#matchactively%E4%B8%BB%E5%8A%A8%E5%8C%B9%E9%85%8D"><code>ItemsMatcherPlugin</code></a></strong> 的特殊命令,用于立即触发 <code>MatchActively</code> 流程。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="match [Bots]">使用</button></td></tr>
<tr>
<td><code>nickname [Bots] <Nickname></code></td>
<td><code>Master</code></td>
<td>将指定机器人的昵称更改为 <code>Nickname</code>。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="nickname [Bots] <Nickname>">使用</button></td></tr>
<tr>
<td><code>owns [Bots] <Games></code></td>
<td><code>Operator</code></td>
<td>检查指定机器人是否已拥有 <code>Games</code>,该参数解释详见<a href="#owns-%E5%91%BD%E4%BB%A4%E7%9A%84-games-%E5%8F%82%E6%95%B0"><strong>下文</strong></a>。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="owns [Bots] <Games>">使用</button></td></tr>
<tr>
<td><code>pause [Bots]</code></td>
<td><code>Operator</code></td>
<td>永久暂停指定机器人的自动挂卡模块。 ASF 在本次会话中将不会再尝试对此帐户进行挂卡,除非您手动 <code>resume</code> 或者重启 ASF。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="pause [Bots]">使用</button></td></tr>
<tr>
<td><code>pause~ [Bots]</code></td>
<td><code>FamilySharing</code></td>
<td>临时暂停指定机器人的自动挂卡模块。 挂卡进程将会在下次游戏事件或者机器人断开连接时自动恢复。 您可以 <code>resume</code> 以恢复挂卡。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="pause~ [Bots]">使用</button></td></tr>
<tr>
<td><code>pause& [Bots] <Seconds></code></td>
<td><code>Operator</code></td>
<td>临时暂停指定机器人的自动挂卡模块 <code>Seconds</code> 秒。 之后,挂卡模块会自动恢复。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="pause& [Bots] <Seconds>">使用</button></td></tr>
<tr>
<td><code>play [Bots] <AppIDs,GameName></code></td>
<td><code>Master</code></td>
<td>切换到手动挂卡——使指定机器人运行给定的 <code>AppIDs</code>,并且可选自定义 <code>GameName</code> 为游戏名称。 若要此功能正常工作,您的 Steam 帐户<strong>必须</strong>拥有所有您指定的 <code>AppIDs</code> 的有效许可,包括免费游戏。 使用 <code>reset</code> 或 <code>resume</code> 命令恢复。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="play [Bots] <AppIDs,GameName>">使用</button></td></tr>
<tr>
<td><code>points [Bots]</code></td>
<td><code>Master</code></td>
<td>显示指定机器人的 <a href="https://store.steampowered.com/points/shop" rel="nofollow"><strong>Steam 点数商店</strong></a>点数余额。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="points [Bots]">使用</button></td></tr>
<tr>
<td><code>privacy [Bots] <Settings></code></td>
<td><code>Master</code></td>
<td>更改指定机器人的 <strong><a href="https://steamcommunity.com/my/edit/settings" rel="nofollow">Steam 隐私设置</a></strong>,可用选项见<a href="#privacy-%E8%AE%BE%E7%BD%AE"><strong>下文</strong></a>。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="privacy [Bots] <Settings>">使用</button></td></tr>
<tr>
<td><code>redeem [Bots] <Keys></code></td>
<td><code>Operator</code></td>
<td>为指定机器人激活给定的游戏序列号或钱包充值码。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="redeem [Bots] <Keys>">使用</button></td></tr>
<tr>
<td><code>redeem^ [Bots] <Modes> <Keys></code></td>
<td><code>Operator</code></td>
<td>以 <code>Modes</code> 模式为指定机器人激活给定的游戏序列号或钱包充值码,模式详见下文的<a href="#redeem-%E6%A8%A1%E5%BC%8F"><strong>解释</strong></a>。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="redeem^ [Bots] <Modes> <Keys>">使用</button></td></tr>
<tr>
<td><code>redeempoints [Bots] <DefinitionIDs></code></td>
<td><code>Operator</code></td>
<td>为指定机器人以 <a href="https://store.steampowered.com/points/shop" rel="nofollow"><strong>Steam 点数</strong></a>兑换给定物品。 默认只允许免费物品,如果要无条件兑换物品,包括付费物品,则需要在每个符合条件的物品 <code>DefinitionID</code> 结尾添加 <code>!</code> 符号。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="redeempoints [Bots] <DefinitionIDs>">使用</button></td></tr>
<tr>
<td><code>reset [Bots]</code></td>
<td><code>Master</code></td>
<td>重置为原始(之前的)游玩状态,用来配合 <code>play</code> 命令的手动挂卡模式使用。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="reset [Bots]">使用</button></td></tr>
<tr>
<td><code>restart</code></td>
<td><code>Owner</code></td>
<td>重新启动 ASF 进程。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="restart">使用</button></td></tr>
<tr>
<td><code>resume [Bots]</code></td>
<td><code>FamilySharing</code></td>
<td>恢复指定机器人的自动挂卡进程。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="resume [Bots]">使用</button></td></tr>
<tr>
<td><code>rmlicense [Bots] <Licenses></code></td>
<td><code>Master</code></td>
<td>为指定机器人移除给定的 <code>Licenses </code>(许可),该参数解释详见<a href="#%E8%AE%B8%E5%8F%AF"><strong>下文</strong></a>。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="rmlicense [Bots] <Licenses>">使用</button></td></tr>
<tr>
<td><code>start [Bots]</code></td>
<td><code>Master</code></td>
<td>启动指定机器人。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="start [Bots]">使用</button></td></tr>
<tr>
<td><code>stats</code></td>
<td><code>Owner</code></td>
<td>显示进程统计信息,例如托管内存用量。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="stats">使用</button></td></tr>
<tr>
<td><code>status [Bots]</code></td>
<td><code>FamilySharing</code></td>
<td>显示指定机器人的状态。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="status [Bots]">使用</button></td></tr>
<tr>
<td><code>std [Bots]</code></td>
<td><code>Master</code></td>
<td>控制 <strong><a href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/SteamTokenDumperPlugin-zh-CN"><code>SteamTokenDumperPlugin</code></a></strong> 的特殊命令,用于触发刷新指定的机器人并立即提交数据。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="std [Bots]">使用</button></td></tr>
<tr>
<td><code>stop [Bots]</code></td>
<td><code>Master</code></td>
<td>停止指定机器人。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="stop [Bots]">使用</button></td></tr>
<tr>
<td><code>tb [Bots]</code></td>
<td><code>Master</code></td>
<td>列出指定机器人的交易黑名单用户。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="tb [Bots]">使用</button></td></tr>
<tr>
<td><code>tbadd [Bots] <SteamIDs64></code></td>
<td><code>Master</code></td>
<td>将给定的 <code>SteamIDs</code> 加入指定机器人的交易黑名单。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="tbadd [Bots] <SteamIDs64>">使用</button></td></tr>
<tr>
<td><code>tbrm [Bots] <SteamIDs64></code></td>
<td><code>Master</code></td>
<td>将给定的 <code>SteamIDs</code>(或使用 <code>all</code> 表示所有)从指定机器人的交易黑名单中移除。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="tbrm [Bots] <SteamIDs64>">使用</button></td></tr>
<tr>
<td><code>transfer [Bots] <TargetBot></code></td>
<td><code>Master</code></td>
<td>将指定机器人的所有 <code>TransferableTypes</code> 社区物品转移到一个目标机器人。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="transfer [Bots] <TargetBot>">使用</button></td></tr>
<tr>
<td><code>transfer@ [Bots] <AppIDs> <TargetBot></code></td>
<td><code>Master</code></td>
<td>将指定机器人的所有符合给定 <code>AppIDs</code> 的 <code>TransferableTypes</code> 社区物品转移到一个目标机器人。 此命令与 <code>transfer%</code> 相反。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="transfer@ [Bots] <AppIDs> <TargetBot>">使用</button></td></tr>
<tr>
<td><code>transfer% [Bots] <AppIDs> <TargetBot></code></td>
<td><code>Master</code></td>
<td>将指定机器人的所有不符合给定 <code>AppIDs</code> 的 <code>TransferableTypes</code> 社区物品转移到一个目标机器人。 此命令与 <code>transfer@</code> 相反。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="transfer% [Bots] <AppIDs> <TargetBot>">使用</button></td></tr>
<tr>
<td><code>transfer^ [Bots] <AppID> <ContextID> <TargetBot></code></td>
<td><code>Master</code></td>
<td>将指定机器人的 <code>ContextID</code> 库存分类中符合给定 <code>AppID</code> 的物品转移到一个目标机器人。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="transfer^ [Bots] <AppID> <ContextID> <TargetBot>">使用</button></td></tr>
<tr>
<td><code>transfer& [Bots] <AppID> <ContextID> <TargetBot> <Rarities></code></td>
<td><code>Master</code></td>
<td>将指定机器人的 <code>ContextID</code> 库存分类中符合给定 <code>AppID</code> 并且符合给定 <strong><a href="#%E5%B7%B2%E7%9F%A5%E7%9A%84%E7%A8%80%E6%9C%89%E5%BA%A6"><code>Rarities</code></a></strong> 的物品转移到一个目标机器人。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="transfer& [Bots] <AppID> <ContextID> <TargetBot> <Rarities>">使用</button></td></tr>
<tr>
<td><code>unpack [Bots]</code></td>
<td><code>Master</code></td>
<td>拆开指定机器人库存中的所有补充包。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="unpack [Bots]">使用</button></td></tr>
<tr>
<td><code>update [Channel]</code></td>
<td><code>Owner</code></td>
<td>在 GitHub 上检查 ASF 新版本,如果可用则更新。 通常这会每隔 <code>UpdatePeriod</code> 自动执行一次。 可选的 <code>Channel</code> 参数指定 <strong><a href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Configuration-zh-CN#updatechannel"><code>UpdateChannel</code></a></strong>,如果未提供,则默认使用全局设置中的值。 <code>Channel</code> 可以用 <code>!</code> 字符结尾,这会强制在指定频道上更新——包括降级等操作。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="update [Channel]">使用</button></td></tr>
<tr>
<td><code>updateplugins [Channel] [Plugins]</code></td>
<td><code>Owner</code></td>
<td>更新指定的插件。 如果插件支持多个更新频道,可选的 <code>Channel</code> 参数允许您选择不同的 <strong><a href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Configuration-zh-CN#updatechannel"><code>UpdateChannel</code></a></strong> 进行更新。 <code>Channel</code> 可以用 <code>!</code> 字符结尾,这会强制在指定频道上更新——包括降级等操作,但具体的功能取决于插件自身。 如果不指定 <code>Plugins</code>,则所有由 <strong><a href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Configuration-zh-CN#pluginsupdatelist"><code>PluginsUpdateList</code></a></strong> 和 <strong><a href="https://github.com/JustArchiNET/ArchiSteamFarm/wiki/Configuration-zh-CN#pluginsupdatemode"><code>PluginsUpdateMode</code></a></strong> 配置判断为允许自动更新的插件都会被更新。 如果您要更新指定插件,特别是已经默认禁用自动更新的,则需要同时提供 <code>Channel</code> 和 <code>Plugins</code> 参数,这样 ASF 就会忽略其自动更新设置,强行更新它们。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="updateplugins [Channel] [Plugins]">使用</button></td></tr>
<tr>
<td><code>version</code></td>
<td><code>FamilySharing</code></td>
<td>显示 ASF 的版本号。</td>
<td><button type="button" class="rh-modal-button rh-asf-use" data-command="version">使用</button></td></tr>
</tbody>
</table>`;
// src/modules/steam/asf.ts
function getASFHeaders(setting = getSteamSettings()) {
const origin = `${setting.asfProtocol}://${setting.asfHost}:${setting.asfPort}`;
return {
accept: "application/json",
"Content-Type": "application/json",
Authentication: setting.asfPassword,
Host: `${setting.asfHost}:${setting.asfPort}`,
Origin: origin,
Referer: `${origin}/page/commands`
};
}
function getASFUrl(setting = getSteamSettings()) {
return `${setting.asfProtocol}://${setting.asfHost}:${setting.asfPort}/Api/Command`;
}
function htmlToElement2(html) {
const template = document.createElement("template");
template.innerHTML = html.trim();
return template.content.firstElementChild ?? document.createElement("div");
}
function showASFRequired() {
showModal({
closeOnClickOutside: false,
className: "swal-user",
icon: "warning",
title: "此功能需要在设置中配置ASF IPC并开启ASF功能!",
buttons: { confirm: "确定" }
});
}
function normalizeASFCommandFromTable(rawCommand, asfBot) {
const command = rawCommand.trim().replace(/^!/, "");
if (!command) return "";
if (asfBot.trim()) {
return `!${command.replace(/\[Bots\]/g, asfBot.trim())}`;
}
return `!${command}`;
}
function asfSend(command = "") {
const setting = getSteamSettings();
if (!setting.asf) {
showASFRequired();
return;
}
const input = document.createElement("input");
input.placeholder = "输入ASF指令";
input.value = command ? `!${command.replace(/^!/, "")}` : "";
const modalPromise = showModal({
closeOnClickOutside: false,
className: "swal-user",
text: "请在下方输入要执行的ASF指令:",
content: input,
buttons: {
test: "连接测试",
redeem: "激活key",
pause: "暂停挂卡",
resume: "恢复挂卡",
"2fa": "获取令牌",
"2faok": "2faok",
more: "更多ASF指令",
confirm: "确定",
cancel: "取消"
}
});
requestAnimationFrame(() => input.focus());
modalPromise.then((value) => {
switch (value) {
case "redeem":
swalRedeem();
break;
case "pause":
case "resume":
case "2fa":
case "2faok":
asfRedeem(`!${value}`);
break;
case "test":
asfTest();
break;
case "more":
showASFCommands();
break;
case null:
case void 0:
break;
default: {
const inputValue2 = input.value.trim();
if (!inputValue2) {
showModal({ closeOnClickOutside: false, title: "ASF指令不能为空!", icon: "warning", buttons: { confirm: "确定" } }).then(() => asfSend(command));
} else {
asfRedeem(inputValue2);
}
}
}
});
}
function showASFCommands() {
const content = htmlToElement2(ASF_COMMANDS_HTML);
content.addEventListener("click", (event) => {
if (!(event.target instanceof Element)) return;
const button = event.target.closest("button.rh-asf-use[data-command]");
if (!button) return;
const setting = getSteamSettings();
const normalizedCommand = normalizeASFCommandFromTable(button.dataset.command ?? "", setting.asfBot ?? "");
if (!normalizedCommand) return;
asfSend(normalizedCommand);
});
showModal({
closeOnClickOutside: false,
className: "swal-user",
text: "ASF指令",
content,
buttons: { confirm: "返回", cancel: "关闭" }
}).then((value) => {
if (value) asfSend();
});
}
function swalRedeem() {
const textarea = document.createElement("textarea");
textarea.id = "keyText";
textarea.className = "asf-output";
showModal({
closeOnClickOutside: false,
className: "swal-user",
title: "请输入要激活的key:",
content: textarea,
buttons: { confirm: "激活", cancel: "返回" }
}).then((value) => {
if (value) {
const keys = extractSteamKeys(textarea.value.trim());
if (keys.length > 0) {
const setting = getSteamSettings();
const asfBot = setting.asfBot ? `${setting.asfBot} ` : "";
asfRedeem(`!redeem ${asfBot}${keys.join(",")}`);
} else {
showModal({ closeOnClickOutside: false, title: "steam key不能为空!", icon: "error", buttons: { confirm: "返回", cancel: "关闭" } }).then((v) => {
if (v) swalRedeem();
});
}
} else {
asfSend();
}
});
}
function asfTest() {
const setting = getSteamSettings();
if (!setting.asf) {
showModal({ closeOnClickOutside: false, title: "请先在设置中开启ASF功能", icon: "warning", buttons: { confirm: "确定" } });
return;
}
const apiUrl = getASFUrl(setting);
showModal({ closeOnClickOutside: false, title: "ASF连接测试", text: `正在尝试连接 "${apiUrl}"`, buttons: { confirm: "确定" } });
void request({
method: "POST",
url: apiUrl,
data: '{"Command":"!stats"}',
responseType: "json",
headers: getASFHeaders(setting)
}).then(({ status, data, text }) => {
if (status === 200) {
if (data?.Success === true && data.Message === "OK" && data.Result) {
showModal({ closeOnClickOutside: false, title: "ASF连接成功!", icon: "success", text: `连接地址 "${apiUrl}"
返回内容 "${data.Result.trim()}"`, buttons: { confirm: "确定" } });
} else if (data?.Message) {
showModal({ closeOnClickOutside: false, title: "ASF连接成功?", icon: "info", text: `连接地址 "${apiUrl}"
返回内容 "${data.Message.trim()}"`, buttons: { confirm: "确定" } });
} else {
showModal({ closeOnClickOutside: false, title: "ASF连接失败!", icon: "error", text: `连接地址 "${apiUrl}"
返回内容 "${text ?? ""}"`, buttons: { confirm: "确定" } });
}
} else {
showModal({ closeOnClickOutside: false, title: `ASF连接失败:${status}`, icon: "error", text: `连接地址 "${apiUrl}"`, buttons: { confirm: "确定" } });
}
});
}
function asfRedeem(command) {
const setting = getSteamSettings();
const apiUrl = getASFUrl(setting);
const textarea = document.createElement("textarea");
textarea.className = "asf-output";
textarea.readOnly = true;
const isRedeemCommand = /!redeem/gim.test(command);
showModal({
closeOnClickOutside: false,
className: "swal-user",
text: `正在执行ASF指令:${command}`,
content: textarea,
buttons: isRedeemCommand ? { confirm: "提取未使用key", cancel: "关闭" } : { confirm: "确定" }
}).then((value) => {
if (!isRedeemCommand) return;
GM_setValue("history", [document.querySelector(".rh-modal-content")?.innerHTML ?? "", textarea.value]);
if (value) {
const unusedKeys = textarea.value.split(/[(\r\n)\r\n]+/).filter((line) => /未使用/gim.test(line)).join(",");
if (unusedKeys) {
GM_setClipboard(extractSteamKeys(unusedKeys).join(","));
showModal({ title: "复制成功!", icon: "success" });
}
}
});
void request({
method: "POST",
url: apiUrl,
data: JSON.stringify({ Command: command }),
responseType: "json",
headers: getASFHeaders(setting)
}).then(({ status, data, text, error }) => {
if (status === 200) {
if (data?.Success && data.Message === "OK" && data.Result) {
textarea.value += `${data.Result.trim()}
`;
} else if (data?.Message) {
textarea.value += `${data.Message.trim()}
`;
} else {
textarea.value += text ?? "";
}
return;
}
showModal({
closeOnClickOutside: false,
className: "swal-user",
title: `执行ASF指令(${command})失败!请检查ASF配置是否正确!`,
text: text || String(error ?? status),
icon: "error",
buttons: { confirm: "关闭" }
});
});
}
function openASFDialog() {
asfSend();
}
// src/modules/steam/steamWeb.ts
var STEAM_HOSTS = {
STORE: "store.steampowered.com",
LOGIN: "login.steampowered.com"
};
function showSwalMessage(options) {
return showModal({ className: "swal-user", closeOnClickOutside: false, ...options });
}
function htmlToElement3(html) {
const template = document.createElement("template");
template.innerHTML = html.trim();
return template.content.firstElementChild ?? document.createElement("div");
}
async function refreshToken() {
const formData = new FormData();
formData.append("redir", `https://${STEAM_HOSTS.STORE}/`);
const response = await request({
url: `https://${STEAM_HOSTS.LOGIN}/jwt/ajaxrefresh`,
method: "POST",
responseType: "json",
headers: {
Host: STEAM_HOSTS.LOGIN,
Origin: `https://${STEAM_HOSTS.STORE}`,
Referer: `https://${STEAM_HOSTS.STORE}/`
},
data: formData
});
if (response.ok && response.data?.success) {
return setStoreToken(response.data);
}
return false;
}
async function setStoreToken(param) {
const formData = new FormData();
formData.append("steamID", param.steamID);
formData.append("nonce", param.nonce);
formData.append("redir", param.redir);
formData.append("auth", param.auth);
const response = await request({
url: `https://${STEAM_HOSTS.STORE}/login/settoken`,
method: "POST",
headers: {
Accept: "application/json, text/plain, */*",
Host: STEAM_HOSTS.STORE,
Origin: `https://${STEAM_HOSTS.STORE}`
},
data: formData
});
return response.status === 200;
}
async function updateStoreAuth(retry = false) {
const response = await request({
url: `https://${STEAM_HOSTS.STORE}/`,
method: "GET",
headers: {
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"Cache-Control": "max-age=0",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Upgrade-Insecure-Requests": "1"
},
fetch: false,
redirect: "manual"
});
const html = response.text ?? "";
if (response.status === 200) {
if (!html.includes("data-miniprofile=")) {
if (await refreshToken()) return retry ? false : updateStoreAuth(true);
return false;
}
const storeSessionID = html.match(/g_sessionID = "(.+?)";/)?.[1];
if (storeSessionID) {
setSteamSessionID(storeSessionID);
return true;
}
return false;
}
if ([301, 302].includes(response.status)) {
if (await refreshToken()) return retry ? false : updateStoreAuth(true);
return false;
}
return false;
}
function createRedeemContent() {
return htmlToElement3(`
<div id="registerkey_examples_text">
<div class="notice_box_content" id="unusedKeyArea">
<b>未使用的Key:</b><br>
<div><ol id="unusedKeys" align="left"></ol></div>
</div>
<div class="table-responsive table-condensed">
<table class="table table-hover hclonely">
<caption><h2>激活记录</h2></caption>
<thead><tr><th>No.</th><th>Key</th><th>结果</th><th>详情</th><th>Sub</th></tr></thead>
<tbody></tbody>
</table>
</div><br>
</div>
`);
}
function webRedeem(keysCsv) {
const redeemContent = createRedeemContent();
showSwalMessage({ title: "正在获取sessionID...", buttons: { confirm: "关闭" } });
if (!getSteamSessionID()) {
handleNoSession(keysCsv, redeemContent);
return;
}
showRedeemDialog(keysCsv, redeemContent);
}
function handleNoSession(keysCsv, redeemContent) {
void request({
method: "GET",
url: "https://store.steampowered.com/account/registerkey"
}).then(async (response) => {
if ((response.response?.finalUrl ?? "").includes("login") && !await updateStoreAuth()) {
showSwalMessage({
title: "请先登录steam!",
icon: "warning",
buttons: { confirm: "登录", cancel: "关闭" }
}).then((value) => {
if (value) window.open("https://store.steampowered.com/login/", "_blank");
});
} else if (response.status === 200) {
setSteamSessionID(response.text?.match(/g_sessionID = "(.+?)";/)?.[1] || "");
showRedeemDialog(keysCsv, redeemContent);
} else {
showSwalMessage({ title: "获取sessionID失败!", icon: "error", buttons: { confirm: "关闭" } });
}
});
}
function showRedeemDialog(keysCsv, redeemContent) {
setRedeemRenderRoot(redeemContent);
showSwalMessage({
title: "正在激活steam key...",
content: redeemContent,
buttons: { confirm: "提取未使用key", cancel: "关闭" }
}).then((value) => {
const modalContent = document.querySelector(".rh-modal-content");
const textareaValue = modalContent?.querySelector("textarea")?.value || "";
GM_setValue("history", [modalContent?.innerHTML || "", textareaValue]);
if (value) {
GM_setClipboard(extractSteamKeys(redeemContent.querySelector("#unusedKeys")?.textContent || "").join(","));
showSwalMessage({ title: "复制成功!", icon: "success" });
}
clearRedeemRenderRoot(redeemContent);
});
redeemKeys(keysCsv);
}
function redeemSub(raw) {
const subText = raw || document.querySelector("#gameSub")?.value;
if (!subText) return;
const ownedPackages = {};
document.querySelectorAll(".account_table a").forEach((link) => {
const match = link.href.match(/javascript:RemoveFreeLicense\( ([0-9]+), '/);
if (match) ownedPackages[Number(match[1])] = true;
});
const freePackages = subText.match(/[\d]{2,}/g) || [];
let loaded = 0;
const total = freePackages.length;
if (total === 0) return;
const showCompletion = () => {
if (window.location.href.includes("licenses")) {
window.open("https://store.steampowered.com/account/licenses/", "_self");
} else {
showModal({
title: "全部激活完成,是否前往账户页面查看结果?",
buttons: { cancel: "取消", confirm: "确定" }
}).then((value) => {
if (value) window.open("https://store.steampowered.com/account/licenses/", "_blank");
});
}
};
const markLoaded = () => {
loaded++;
if (loaded >= total) {
showCompletion();
} else {
showModal("正在激活…", `进度:${loaded}/${total}.`);
}
};
showModal("正在执行…", "请等待所有请求完成。 忽略所有错误,让它完成。");
freePackages.forEach((packageText) => {
const packageId = Number.parseInt(packageText, 10);
if (ownedPackages[packageId]) {
markLoaded();
return;
}
void request({
url: "https://store.steampowered.com/checkout/addfreelicense",
method: "POST",
data: new URLSearchParams({ action: "add_to_cart", sessionid: getSteamSessionID() || safeGlobalSessionID(), subid: String(packageId) }),
headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }
}).then(markLoaded);
});
}
function redeemSubs() {
const key = document.querySelector("#inputKey")?.value.trim();
if (key) redeemSub(key);
}
function safeGlobalSessionID() {
try {
return typeof g_sessionID === "string" ? g_sessionID : "";
} catch {
return "";
}
}
function changeStoreCountryFlow() {
void fetchCartData().then((cartData) => {
const { cartConfig, userInfo } = parseCartData(cartData);
if (!cartConfig || !userInfo || Object.keys(cartConfig.rgUserCountryOptions).length <= 2) {
showSwalMessage({ title: "需要挂相应地区的梯子!", icon: "warning" });
return;
}
showCountryChangeDialog(cartConfig, userInfo, cartData);
}).catch(() => showSwalMessage({ title: "获取当前国家/地区失败!", icon: "error" }));
showSwalMessage({ title: "正在获取当前国家/地区...", icon: "info" });
}
function fetchCartData() {
return request({ url: "https://store.steampowered.com/cart/", method: "GET" }).then((response) => {
if (!response.ok && !response.text) throw new Error("Failed to fetch cart");
return response.text ?? "";
});
}
function decodeHtml(value) {
const temp = document.createElement("div");
temp.innerHTML = value;
return temp.textContent || temp.innerText || "";
}
function parseCartData(data) {
const cartConfig = JSON.parse(decodeHtml(data.match(/data-cart_config="(.*?)"/)?.[1] || ""));
const userInfo = JSON.parse(decodeHtml(data.match(/data-userinfo="(.*?)"/)?.[1] || ""));
return { cartConfig, userInfo };
}
function bindCurrencyChangeOption() {
const intervalId = window.setInterval(() => {
const options = document.querySelectorAll(".currency_change_option");
if (options.length > 0) {
options.forEach((option) => option.addEventListener("click", () => {
const newCountry = option.dataset.country;
if (newCountry) changeCountry(newCountry);
}));
window.clearInterval(intervalId);
}
}, 500);
window.setTimeout(() => window.clearInterval(intervalId), 1e4);
}
function showCountryChangeDialog(cartConfig, userInfo, cartData) {
const divContent = cartData.match(/<div class="currency_change_options">([\w\W]*?)<p/i)?.[1]?.trim();
const div = `${divContent || ""}</div>`;
showSwalMessage({
closeOnClickOutside: false,
title: `当前国家/地区:${cartConfig.rgUserCountryOptions[userInfo.country_code] || userInfo.country_code}`,
content: htmlToElement3(`<div>${div}</div>`)
});
bindCurrencyChangeOption();
}
function changeCountry(country) {
showSwalMessage({ closeOnClickOutside: false, icon: "info", title: "正在更换国家/地区..." });
void request({
url: "https://store.steampowered.com/country/setcountry",
method: "POST",
data: new URLSearchParams({ sessionid: getSteamSessionID() || safeGlobalSessionID(), cc: country }),
headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" }
}).then(() => {
void fetchCartData().then((data) => {
const { cartConfig, userInfo } = parseCartData(data);
const divContent = data.match(/<div class="currency_change_options">([\w\W]*?)<p/i)?.[1]?.trim();
const div = `${divContent || ""}</div>`;
if (userInfo.country_code === country) {
showSwalMessage({ title: "更换成功!", icon: "success" }).then(() => {
showSwalMessage({
closeOnClickOutside: false,
title: `当前国家/地区:${cartConfig.rgUserCountryOptions[userInfo.country_code] || userInfo.country_code}`,
content: htmlToElement3(`<div>${div}</div>`)
});
bindCurrencyChangeOption();
});
} else {
showSwalMessage({ title: "更换失败!", icon: "error" });
}
}).catch(() => showSwalMessage({ title: "获取当前国家/地区失败!", icon: "error" }));
});
}
// src/modules/steam/redeem.ts
var FAILURE_DETAILS = {
14: "无效激活码",
15: "重复激活",
53: "次数上限",
13: "地区限制",
9: "已拥有",
24: "缺少主游戏",
36: "需要PS3?",
50: "这是充值码"
};
var UNUSED_KEY_REASONS = ["次数上限", "地区限制", "已拥有", "缺少主游戏", "其他错误", "未知错误", "网络错误或超时"];
var AUTO_DIVIDE_NUM = 9;
var WAITING_SECONDS = 20;
var state = {
allUnusedKeys: [],
keyCount: 0,
recvCount: 0,
renderRoot: null,
popupFlow: false,
sessionID: ""
};
var texts = {
fail: "失败",
success: "成功",
network: "网络错误或超时",
line: "——",
nothing: "",
others: "其他错误",
redeeming: "激活中",
waiting: "等待中"
};
function getSessionID() {
if (state.sessionID) return state.sessionID;
try {
state.sessionID = typeof g_sessionID === "string" ? g_sessionID : "";
} catch {
state.sessionID = "";
}
return state.sessionID;
}
function setSteamSessionID(sessionID) {
state.sessionID = sessionID;
}
function getSteamSessionID() {
return getSessionID();
}
function setRedeemRenderRoot(root, popupFlow = true) {
state.renderRoot = root;
state.popupFlow = popupFlow;
state.keyCount = 0;
state.recvCount = 0;
state.allUnusedKeys = [];
}
function clearRedeemRenderRoot(root) {
if ((!root || state.renderRoot === root) && state.recvCount >= state.keyCount) {
state.renderRoot = null;
state.popupFlow = false;
}
}
function queryRedeemRoot(selector) {
return (state.renderRoot ?? document).querySelector(selector);
}
function queryRedeemRootAll(selector) {
return (state.renderRoot ?? document).querySelectorAll(selector);
}
function table() {
return queryRedeemRoot("table");
}
function tbody() {
return queryRedeemRoot("tbody");
}
function createCell(tag, html, className) {
const cell = document.createElement(tag);
if (className) cell.className = className;
cell.innerHTML = html;
return cell;
}
function createSubCell(subId, subName) {
const cell = document.createElement("td");
if (subId === 0) {
cell.textContent = "——";
return cell;
}
const code = document.createElement("code");
code.textContent = String(subId);
const link = document.createElement("a");
link.href = `https://steamdb.info/sub/${subId}/`;
link.target = "_blank";
link.textContent = subName;
cell.append(code, " ", link);
return cell;
}
function setUnusedKeys(key, success, reason, subId, subName) {
const unusedKeys = queryRedeemRoot("#unusedKeys");
if (!unusedKeys) return;
if (success && state.allUnusedKeys.includes(key)) {
state.allUnusedKeys = state.allUnusedKeys.filter((keyItem) => keyItem !== key);
unusedKeys.querySelectorAll("li").forEach((li) => {
if (li.textContent?.includes(key)) li.remove();
});
} else if (!success && !state.allUnusedKeys.includes(key) && UNUSED_KEY_REASONS.includes(reason)) {
const li = document.createElement("li");
li.append(`${key} (${reason}`);
if (subId !== 0) {
li.append(": ");
const code = document.createElement("code");
code.textContent = String(subId);
li.append(code, ` ${subName}`);
}
li.append(")");
unusedKeys.append(li);
state.allUnusedKeys.push(key);
}
}
function tableInsertKey(key) {
state.keyCount++;
const row = document.createElement("tr");
row.append(createCell("td", String(state.keyCount), "nobr"));
row.append(createCell("td", `<code>${key}</code>`, "nobr"));
const waitCell = createCell("td", `${texts.redeeming}...`);
waitCell.colSpan = 3;
row.append(waitCell);
tbody()?.prepend(row);
}
function tableWaitKey(key) {
state.keyCount++;
const row = document.createElement("tr");
row.append(createCell("td", String(state.keyCount), "nobr"));
row.append(createCell("td", `<code>${key}</code>`, "nobr"));
const waitCell = createCell("td", `${texts.waiting} (${WAITING_SECONDS}秒)...`);
waitCell.colSpan = 3;
row.append(waitCell);
tbody()?.prepend(row);
}
function tableUpdateKey(key, result, detail, subId, subName) {
setUnusedKeys(key, result === texts.success, detail, subId, subName);
state.recvCount++;
if (!state.popupFlow && state.recvCount === state.keyCount) {
document.querySelector("#buttonRedeem, #redeemKey")?.style.removeProperty("display");
document.querySelector("#inputKey")?.removeAttribute("disabled");
}
const rows = Array.from(queryRedeemRootAll("table tr")).slice(1);
for (const row of rows) {
const cells = row.children;
if (cells[1]?.innerHTML.includes(key) && cells[2]?.innerHTML.includes(texts.redeeming)) {
cells[2].remove();
const resultCell = createCell("td", result, "nobr");
resultCell.style.color = result === texts.fail ? "red" : "green";
row.append(resultCell);
row.append(createCell("td", detail, "nobr"));
row.append(createSubCell(subId, subName));
break;
}
}
}
function redeemKey(key) {
void request({
url: "https://store.steampowered.com/account/ajaxregisterkey/",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
Origin: "https://store.steampowered.com",
Referer: "https://store.steampowered.com/account/registerkey"
},
data: `product_key=${encodeURIComponent(key)}&sessionid=${encodeURIComponent(getSessionID())}`,
method: "POST",
responseType: "json",
onloadstart: () => {
const currentTable = table();
if (currentTable && currentTable.style.display === "none") currentTable.style.display = "";
}
}).then((response) => {
if (response.status === 200 && response.data) {
const data = response.data;
if (data.success === 1 && data.purchase_receipt_info?.line_items[0]) {
const item = data.purchase_receipt_info.line_items[0];
tableUpdateKey(key, texts.success, texts.line, item.packageid, item.line_item_description);
return;
}
if (data.purchase_result_details !== void 0 && data.purchase_receipt_info) {
const item = data.purchase_receipt_info.line_items[0];
const failureReason = FAILURE_DETAILS[data.purchase_result_details] || texts.others;
tableUpdateKey(key, texts.fail, failureReason, item?.packageid ?? 0, item?.line_item_description ?? texts.nothing);
return;
}
tableUpdateKey(key, texts.fail, texts.nothing, 0, texts.nothing);
} else {
tableUpdateKey(key, texts.fail, texts.network, 0, texts.nothing);
}
});
}
function startTimer() {
const timer = window.setInterval(() => {
let hasWaiting = false;
let nowKey = 0;
const rows = Array.from(queryRedeemRootAll("table tr")).slice(1).reverse();
for (const row of rows) {
const cell = row.children[2];
if (cell?.innerHTML.includes(texts.waiting)) {
nowKey++;
if (nowKey <= AUTO_DIVIDE_NUM) {
const key = row.children[1]?.textContent?.trim() ?? "";
cell.innerHTML = `${texts.redeeming}...`;
redeemKey(key);
} else {
hasWaiting = true;
break;
}
}
}
if (!hasWaiting) window.clearInterval(timer);
}, 1e3 * WAITING_SECONDS);
}
function redeem(keys) {
if (keys.length <= 0) return;
if (!state.popupFlow) {
document.querySelector("#buttonRedeem, #redeemKey")?.style.setProperty("display", "none");
document.querySelector("#inputKey")?.setAttribute("disabled", "disabled");
}
let nowKey = 0;
keys.forEach((key) => {
nowKey++;
if (nowKey <= AUTO_DIVIDE_NUM) {
tableInsertKey(key);
redeemKey(key);
} else {
tableWaitKey(key);
}
});
if (nowKey > AUTO_DIVIDE_NUM) startTimer();
}
function redeemKeys(key) {
const keys = key ? key.split(",").map((item) => item.trim()).filter(Boolean) : extractSteamKeys(document.querySelector("#inputKey")?.value.trim() || "");
redeem(keys);
}
function registerSteamKeys(raw) {
const setting = getSteamSettings();
const keys = extractSteamKeys(raw);
if (keys.length === 0) return;
if (setting.asf) {
const asfCommand = `!redeem ${setting.asfBot ? `${setting.asfBot} ` : ""}${keys.join(",")}`;
asfRedeem(asfCommand);
} else if (setting.newTab) {
window.open(`https://store.steampowered.com/account/registerkey?key=${keys.join(",")}`, "_blank");
} else {
webRedeem(keys.join(","));
}
}
function copyUnusedKeys() {
GM_setClipboard(extractSteamKeys(queryRedeemRoot("#unusedKeys")?.textContent || "").join(","));
showModal({ title: "复制成功!", icon: "success" });
}
function toggleUnusedKeyArea() {
if (!state.popupFlow) {
const unusedKeyArea = queryRedeemRoot("#unusedKeyArea");
if (unusedKeyArea) unusedKeyArea.style.display = unusedKeyArea.style.display === "none" ? "" : "none";
}
}
function initSteamRedeemPage() {
state.renderRoot = null;
state.popupFlow = false;
state.keyCount = 0;
state.recvCount = 0;
state.allUnusedKeys = [];
getSessionID();
const examples = document.querySelector("#registerkey_examples_text");
if (examples) {
examples.innerHTML = `
<div class="notice_box_content" id="unusedKeyArea" style="display: none">
<b>未使用的Key:</b>
<a tabindex="300" class="btnv6_blue_hoverfade btn_medium" id="copyUnuseKey"><span>提取未使用key</span></a><br>
<div><ol id="unusedKeys"></ol></div>
</div>
<div class="table-responsive table-condensed">
<table class="table table-hover" style="display: none">
<caption><h2>激活记录</h2></caption>
<thead><tr><th>No.</th><th>Key</th><th>结果</th><th>详情</th><th>Sub</th></tr></thead>
<tbody></tbody>
</table>
</div><br>`;
setRedeemRenderRoot(examples, false);
}
const inputBox = document.querySelector(".registerkey_input_box_text")?.parentElement;
inputBox?.style.setProperty("float", "none");
inputBox?.insertAdjacentHTML("beforeend", '<textarea class="form-control" rows="3" id="inputKey" placeholder="支持批量激活,可以把整个网页文字复制过来 若一次激活的Key的数量超过9个则会自动分批激活(等待20秒) 激活多个SUB时每个SUB之间用英文逗号隔开" style="margin: 3px 0px 0px; width: 525px; height: 102px;"></textarea><br>');
const keyFromUrl = new URL(window.location.href).searchParams.get("key");
if (keyFromUrl) {
const input = document.querySelector("#inputKey");
if (input) input.value = keyFromUrl;
}
document.querySelectorAll(".registerkey_input_box_text,#purchase_confirm_ssa").forEach((el) => {
el.style.display = "none";
});
const registerButton = document.querySelector("#register_btn");
registerButton?.parentElement?.style.setProperty("margin", "10px 0");
registerButton?.parentElement?.insertAdjacentHTML("beforeend", `
<a tabindex="300" class="btnv6_blue_hoverfade btn_medium" style="margin-left:0" id="redeemKey"><span>激活key</span></a>
<a tabindex="300" class="btnv6_blue_hoverfade btn_medium" style="margin-left:0" id="redeemSub"><span>激活sub</span></a>
<a tabindex="300" class="btnv6_blue_hoverfade btn_medium" style="margin-left:0" id="changeCountry"><span>更换国家/地区</span></a> `);
registerButton?.remove();
document.querySelector("#copyUnuseKey")?.addEventListener("click", copyUnusedKeys);
document.querySelector("#redeemKey")?.addEventListener("click", () => redeemKeys());
if (keyFromUrl) redeem(extractSteamKeys(keyFromUrl));
toggleUnusedKeyArea();
}
function bindCopySelectClickListeners() {
const setting = getSteamSettings();
if (setting.selectListen) {
bindSelectListener();
}
if (!/https?:\/\/store\.steampowered\.com\/account\/registerkey/.test(window.location.href) && setting.copyListen) {
window.addEventListener("copy", activateCopiedProduct, false);
}
if (setting.clickListen) {
bindClickListener();
}
}
function activateCopiedProduct(event) {
const setting = getSteamSettings();
const productKey = window.getSelection()?.toString()?.trim() || event.target?.value || "";
void navigator.clipboard?.writeText(productKey).catch(() => void 0);
if (/^([\w\W]*)?([\d\w]{5}(-[\d\w]{5}){2}(\r|,|,)?){1,}/.test(productKey)) {
if (!document.querySelector("div.rh-modal-overlay")) {
showModal({ title: "检测到神秘key,是否激活?", icon: "success", buttons: { confirm: "激活", cancel: "取消" } }).then((value) => {
if (value) registerSteamKeys(productKey);
});
}
} else if ((/^![\w\d]+\s+asf\s+.+/gi.test(productKey) || /^!ALA\s+.+/gi.test(productKey)) && setting.asf) {
if (!document.querySelector("div.rh-modal-overlay")) {
showModal({ closeOnClickOutside: false, className: "swal-user", title: "检测到您复制了以下ASF指令,是否执行?", text: productKey, buttons: { confirm: "执行", cancel: "取消" } }).then((value) => {
if (value) asfRedeem(productKey);
});
}
}
}
function bindSelectListener() {
const icon = document.createElement("div");
icon.className = "icon-div";
icon.title = "激活";
icon.innerHTML = '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAABsFBMVEVHcEz9/f3+/v8Tdaf///8LGTP+/v4NPWX+/v8GGDj8/f4OGzX///////////8ZJ1ALJ0oNGzf+/v4mOGXX2+T8/f3z9vkPH0UjMFj5+fr///8NGzMJG0ERHS8SKEgXW40Ubp8WY5Ula5ePnrb3+PlnhKUfQ3Q3R3C2vMvt8PRcZH7///8TgrMTgbITfa4YNmcPHDT///8JGj0HGT2rucxGWYJ0g6H///9aZYbGy9jFyNQhL1TO0dp5lbOTma39/f7///+Sl6br7O6SlqT///////8UY5UUY5UJGTgTc6QThLUTfa8VToETdKYQK1sRK1sUWowUY5X///+mq7tlc5MzWX7Mztl5gZuytcJPV3AjL08iLlEUTYAUPXATK10Tfa4FO3IVToHEz9wAL2fj5+5GUG4JFz1mbIC8wMsFFzYFGDwHGkILGTIJG0cLHUoFGD8RJFMUU4YUN2kOIE4TSHsUYJMUbqAUQnUVTYAQLWEUWowUPG8SKFoTZ5kRHDATfq8Tdqf///8IIFQUaJkeWYrA0N4JMmgHQndAc5wJTYIIVooGXpNllrdchanT3ui2xtaZV9eJAAAAaXRSTlMAj/j97/vmAtD8xzncwJvwJay59uHd/ObqgnBtlpgLPfz8Femx+/7s0vitXqVLf53k3VmE9fDr59vaxLSY/qmGRnxeXjI/lGy5MLbr+8A/cPuiqLzgfLHJhJfN17bjr6z8xfmu9cH4cHitMXlOAAACjUlEQVQ4y2XTh1faQBgA8IMGQqCiGPaQAgUE3OLeFTfurR120SYkAQSrQUABUSyOf7l3kQC+/i7JfXff9/Ly4DsAXuA4AG0qi02jsVlUbS/rRnCpsvXkqnpsKmGrnsZBy2KOyTGyHBrooWlpeAkMLAzDyBiRDIayvloFnKwMJSOoKgJGsghD9YsVOLASBEE3pKtz/8t34MBCEXQddbQ7O/trjKAiVB96BY630I2ow1BobjoUmjXQdAR+KRyL0Ug0VhWlD+c+jBkMxzNzM3BXgwrWog1oYyBgODr47adnpv2xSBT+HkDDRlgxz9H+wM/jgNkc8BvNBzGWtQLgNLAcy8JbGLFJs3HSZDJNmQzjpmaWM7SBviRXk2SbxyXGSQkCI1jAqUB/Mp6sYbfV6mGvWq2W7JOSPQ7urIPuJBsXccNbkNG7t7cf397aQQkrMLUPZLh4JoMushV5P0yS3vatUbSV+QwkytaRQbJQ4DOFwqiySQnBR1PTKAnTPCyY0ikUSoVrgPcOjihE2vYdPnPK8yTfDczytwKtbsQVhHOvTqcd4Hn+VMCvg2lM3itHMK3LMyiXD/UGPbeXkFBwuQacrh8Yhg1hiM8TxHxu/vT8UvQJtud3j/ubVKCXTrix8O15Xaob/hf21MlJOZiX5qXSvHvi+Tacqgmn7Kgfvl6E7+58ped8vpTyTdwlLkThi49CP9gTiZv7R/1juXxTerpPNEINA6/d9Eb6/kH/VNKXbtJ1G+kFsSk3zxyOv46Hh3KlclbjOJsHYleDzWKxmK1UskUkixSzxfnGc9f1DvkjQvHCq6OHL61eX1+/qYLR6tKrw4lKOr+sXFWtdNj/O99wiTs7uzqWlzu6Op14Pf0P2PD9NrHDeWsAAAAASUVORK5CYII=" class="icon-img" alt="激活图标">';
document.documentElement.append(icon);
document.addEventListener("selectionchange", () => {
if (!window.getSelection()?.toString()?.trim()) icon.style.display = "none";
});
document.addEventListener("mouseup", (event) => {
if (event.target === icon || icon.contains(event.target)) {
event.preventDefault();
return;
}
const text = window.getSelection()?.toString()?.trim() || "";
const productKey = text || event.target?.value || "";
if (/[\d\w]{5}(-[\d\w]{5}){2}/.test(productKey) && text && icon.style.display === "none") {
icon.style.top = `${event.pageY + 12}px`;
icon.style.left = `${event.pageX + 18}px`;
icon.style.display = "block";
} else if (!text) {
icon.style.display = "none";
}
});
icon.addEventListener("mousedown", (event) => event.preventDefault());
icon.addEventListener("click", (event) => {
const productKey = window.getSelection()?.toString()?.trim() || event.target?.value || "";
registerSteamKeys(productKey);
});
}
function bindClickListener() {
document.body?.addEventListener("click", (event) => {
const htmlEl = event.target;
if (!htmlEl || htmlEl.closest(".rh-modal-overlay") || ["A", "BUTTON", "TEXTAREA"].includes(htmlEl.tagName) || ["button", "text"].includes(htmlEl.getAttribute("type") || "")) return;
if (htmlEl.children.length > 0 && extractSteamKeys(Array.from(htmlEl.children).map((child) => child.textContent ?? "").join("")).length > 0) return;
const keys = extractSteamKeys(htmlEl.textContent ?? "");
if (keys.length === 0) return;
mouseClick(event);
let html = htmlEl.innerHTML;
keys.forEach((key) => {
html = html.replace(new RegExp(key, "gi"), `<a class="redee-key" href="javascript:void(0)" target="_self" data-key="${key}">${key}</a>`);
});
htmlEl.innerHTML = html;
htmlEl.querySelectorAll(".redee-key").forEach((link) => {
link.addEventListener("click", () => registerSteamKeys(link.dataset.key || ""));
});
});
}
function mouseClick(event) {
const span = document.createElement("span");
span.textContent = "Steam Key";
span.style.cssText = `z-index:2147483647;top:${event.pageY - 20}px;left:${event.pageX}px;position:absolute;font-weight:bold;color:#ff6651;transition:all 1.5s ease;`;
document.body.append(span);
requestAnimationFrame(() => {
span.style.top = `${event.pageY - 180}px`;
span.style.opacity = "0";
});
window.setTimeout(() => span.remove(), 1500);
}
// src/modules/steam/index.ts
var STEAM_CSS = `
table.hclonely { font-family: verdana,arial,sans-serif; font-size: 11px; color: #333333; border-width: 1px; border-color: #999999; border-collapse: collapse; }
table.hclonely th { background-color: #c3dde0; border-width: 1px; padding: 8px; border-style: solid; border-color: #a9c6c9; }
table.hclonely tr { background-color: #d4e3e5; }
table.hclonely td { border-width: 1px; padding: 8px; border-style: solid; border-color: #a9c6c9; }
table.hclonely caption { padding-top: 8px; color: #808294; text-align: center; caption-side: top; background-color: #94d7df; }
table.hclonely h2 { margin: 0; font-size: 25px; }
.rh-modal.swal-user { width: 80%; }
table.hclonely a { color: #2196F3; }
table.hclonely .rh-modal-button { padding: 5px; }
#unusedKeyArea code { padding: 2px 4px; font-size: 90%; color: #c7254e; background-color: #f9f2f4; border-radius: 3px; }
.notice_box_content { border: 1px solid #a25024; border-radius: 3px; color: #acb2b8; font-size: 14px; font-family: "Motiva Sans", Sans-serif; font-weight: normal; padding: 15px 15px; margin-bottom: 15px; }
.notice_box_content b { font-weight: normal; color: #f47b20; float: left; }
#unusedKeys { margin:0 15px; }
#copyUnuseKey span { font-size: 15px; line-height: 20px; }
#unusedKeyArea li { white-space: nowrap; color: #007fff; }
.currency_change_option_ctn { vertical-align: top; margin: 0 6%; }
.currency_change_option_ctn:first-child { margin-bottom: 12px; }
.currency_change_option_ctn > p { font-size: 12px; margin: 8px 8px 0 8px; }
.currency_change_option { font-family: "Motiva Sans", Sans-serif; font-weight: 300; display: block; }
.currency_change_option > span { display: block; padding: 9px 19px; }
.currency_change_option .country { font-size: 20px; }
.currency_change_option .notes { font-size: 13px; line-height: 18px; }
.asf-class input[type="text"] { border: 1px solid #c2e9ee; width:180px; }
.asf-output { width:90%; min-height:150px; }
.switch-key { margin:0 15%; height:100px; }
.switch-key-left { float:left; }
.switch-key-right { float:right; }
.switch-key div { width: 50%; position: relative; cursor:default; }
.switch-key input { margin:10px 0; }
.switch-key p { font-size:25px; height:25px; color:black; margin:0; }
.rh-modal-content * { color:#000; }
.rh-modal-content textarea { background: #fff; }
#allKey { display: inline-block; padding: 6px 12px; margin-bottom: 0; font-size: 14px; font-weight: 400; line-height: 1.42857143; text-align: center; white-space: nowrap; vertical-align: middle; cursor: pointer; user-select: none; background-image: none; border: 1px solid #ccc; border-radius: 4px; color: #333; background-color: #fff; }
#allKey:hover, #allKey:focus { color: #333; background-color: #e6e6e6; border-color: #adadad; text-decoration: none; }
.icon-img { position: absolute; width: 32px; height: 32px; margin: 0!important; }
.icon-div { width: 32px!important; height: 32px!important; display: none; background: #fff!important; border-radius: 16px!important; box-shadow: 4px 4px 8px #888!important; position: absolute!important; z-index: 2147483647!important; cursor: pointer; }
`;
var initialized4 = false;
function initSteam() {
if (initialized4) return;
initialized4 = true;
try {
GM_addStyle(STEAM_CSS);
const url = window.location.href;
if (/^https?:\/\/store\.steampowered\.com\/account\/registerkey/.test(url)) {
initSteamRedeemPage();
document.querySelector("#redeemSub")?.addEventListener("click", redeemSubs);
document.querySelector("#changeCountry")?.addEventListener("click", changeStoreCountryFlow);
return;
}
if (/https?:\/\/steamdb\.info\/freepackages\//.test(url)) {
bindSteamDBFreePackages();
return;
}
if (/https?:\/\/store\.steampowered\.com\/account\/licenses\/(\?sub=[\w\W]{0,})?/.test(url)) {
initSteamLicensesPage();
return;
}
bindCopySelectClickListeners();
bindStoreCountryShortcut();
if (getSteamSettings().allKeyListen) {
redeemAllPageKeys();
}
} catch (error) {
showModal("AuTo Redeem Steamkey脚本执行出错,详情请查看控制台!", error.stack, "error");
console.error(error);
}
}
function openSteamSettings() {
openSteamSettingsDialog();
}
function runSteamASF() {
openASFDialog();
}
function bindSteamDBFreePackages() {
const interval = window.setInterval(() => {
const freePackages = document.querySelector("#freepackages");
if (!freePackages) return;
freePackages.addEventListener("click", () => {
const subs = Array.from(document.querySelectorAll("#freepackages span")).filter((span) => span.offsetParent !== null).map((span) => span.dataset.subid || span.getAttribute("data-subid") || "").filter(Boolean);
window.open(`https://store.steampowered.com/account/licenses/?sub=${subs.join(",")}`, "_self");
});
window.clearInterval(interval);
}, 1e3);
}
function initSteamLicensesPage() {
document.querySelector("h2.pageheader")?.parentElement?.insertAdjacentHTML("beforeend", `
<div style="float: left;">
<textarea class="registerkey_input_box_text" rows="1" name="product_key" id="gameSub" placeholder="输入SUB,多个SUB之间用英文逗号连接" style="margin: 3px 0px 0px; width: 400px; height: 15px;background-color:#102634; padding: 6px 18px 6px 18px; font-weight:bold; color:#fff;"></textarea>
</div>
<a tabindex="300" class="btnv6_blue_hoverfade btn_medium" style="width: 95px; height: 30px;" id="buttonSUB"><span>激活SUB</span></a>
<a tabindex="300" class="btnv6_blue_hoverfade btn_medium" style="width: 125px; height: 30px;margin-left:5px" id="changeCountry-account"><span>更改国家/地区</span></a>`);
document.querySelector("#buttonSUB")?.addEventListener("click", () => redeemSub());
document.querySelector("#changeCountry-account")?.addEventListener("click", changeStoreCountryFlow);
if (/https?:\/\/store\.steampowered\.com\/account\/licenses\/\?sub=([\d]+,)+/.test(window.location.href)) {
window.setTimeout(() => redeemSub(window.location.href), 2e3);
}
}
function bindStoreCountryShortcut() {
if (!/https?:\/\/store\.steampowered\.com\//.test(window.location.href)) return;
const accountPulldown = document.querySelector("#account_pulldown");
if (!accountPulldown || document.querySelector("#changeCountry")) return;
accountPulldown.insertAdjacentHTML("beforebegin", '<span id="changeCountry" style="cursor:pointer;display:inline-block;padding-left:4px;line-height:25px" class="global_action_link persona_name_text_content">更改国家/地区 |</span>');
document.querySelector("#changeCountry")?.addEventListener("click", changeStoreCountryFlow);
}
function redeemAllPageKeys() {
const div = document.createElement("div");
div.id = "keyDiv";
div.style.cssText = "position:fixed;left:5px;bottom:5px";
const button = document.createElement("button");
button.id = "allKey";
button.className = "btn btn-default";
button.style.display = "none";
button.style.zIndex = "9999";
button.textContent = "激活本页面所有key(共0个)";
div.append(button);
document.body.append(div);
let previousKeyList = "";
window.setInterval(() => {
const keys = extractSteamKeys(document.body.textContent || "");
if (keys.length > 0) {
const keyList = keys.join(",");
if (previousKeyList !== keyList) {
previousKeyList = keyList;
button.dataset.key = keyList;
button.textContent = `激活本页面所有key(共${keys.length}个)`;
button.style.display = "block";
}
} else if (button.style.display === "block") {
previousKeyList = "";
button.style.display = "none";
button.textContent = "激活本页面所有key(共0个)";
}
}, 1e3);
button.addEventListener("click", () => registerSteamKeys(button.dataset.key || ""));
}
// src/shared/menu.ts
function registerMenus(handlers) {
const wrapMenuHandler = (handler) => () => {
if (window.self !== window.top) {
return;
}
handler();
};
if (handlers.onOpenSettings) {
GM_registerMenuCommand("⚙Steam设置", wrapMenuHandler(handlers.onOpenSettings));
}
if (handlers.onSteamASF) {
GM_registerMenuCommand("执行ASF指令", wrapMenuHandler(handlers.onSteamASF));
}
if (handlers.onIGBatch) {
GM_registerMenuCommand("入库所有IndieGala链接", wrapMenuHandler(handlers.onIGBatch));
}
if (handlers.onItchExtract) {
GM_registerMenuCommand("入库所有ItchIo链接", wrapMenuHandler(handlers.onItchExtract));
}
if (handlers.onGOGBatch) {
GM_registerMenuCommand("领取所有GOG链接", wrapMenuHandler(handlers.onGOGBatch));
}
}
// src/main.ts
function bootstrap() {
initSteam();
initIG();
initItch();
initGOG();
registerMenus({
onOpenSettings: openSteamSettings,
onSteamASF: runSteamASF,
onIGBatch: runIGBatch,
onItchExtract: runItchExtract
// onGOGBatch: runGOGBatch,
});
}
bootstrap();
})();