Greasy Fork

Greasy Fork is available in English.

Steam历史最低价格查询

在steam商店页和愿望单页显示当前app历史最低价格及进包次数(此脚本需要勾选Steam商店加速才会生效)

当前为 2025-09-30 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Steam历史最低价格查询
// @namespace   SteamHistoryLowestPrice
// @description 在steam商店页和愿望单页显示当前app历史最低价格及进包次数(此脚本需要勾选Steam商店加速才会生效)
// @include      https://store.steampowered.com/app/*
// @include      https://store.steampowered.com/bundle/*
// @include      https://store.steampowered.com/sub/*
// @include      https://store.steampowered.com/wishlist/*
// @author      软妹币玩家、byzod、StarRain
// @license     GPL version 3 or any later version
// @version     2.0
// @grant       GM_xmlhttpRequest
// @enable      true
// jshint esversion:6
// ==/UserScript==

// 显示样式
// 0 = 显示在购买按钮上面
// 1 = 显示在购买信息框上面
const INFO_STYLE = 1;

// 货币区域覆盖,两个字母的国家代号,大小写均可
// 空字符("")代表不覆盖,使用steam的cookie中steamCountry的值
// 见 https://zh.wikipedia.org/wiki/ISO_3166-1 或 https://en.wikipedia.org/wiki/ISO_3166-1
// 常用 美国USD:"us", 中国CNY: "cn", 英国GBP: "uk", 日本JPY: "jp", 俄国RUS: "ru"
const CC_OVERRIDE = "";

// 货币符号
const CURRENCY_SYMBOLS = {
    'AED': 'DH',
    'AUD': 'A$',
    'BRL': 'R$',
    'CAD': 'CDN$',
    'CHF': 'CHF',
    'CLP': 'CLP$',
    'CNY': '¥',  // Chines Yuan
    'COP': 'COL$',
    'CRC': '₡',    // Costa Rican Colón
    'EUR': '€',    // Euro
    'GBP': '£',    // British Pound Sterling
    'HKD': 'HK$',
    'IDR': 'Rp',
    'ILS': '₪',    // Israeli New Sheqel
    'INR': '₹',    // Indian Rupee
    'JPY': '¥',    // Japanese Yen
    'KRW': '₩',    // South Korean Won
    'MXN': 'Mex$',
    'MYR': 'RM',
    'NGN': '₦',    // Nigerian Naira
    'NOK': 'kr',
    'NZD': 'NZ$',
    'PEN': 'S/.',
    'PHP': '₱',    // Philippine Peso
    'PLN': 'zł',   // Polish Zloty
    'PYG': '₲',    // Paraguayan Guarani
    'RUB': 'pуб',
    'SAR': 'SR',
    'SGD': 'S$',
    'THB': '฿',    // Thai Baht
    'TRY': 'TL',
    'TWD': 'NT$',
    'UAH': '₴',    // Ukrainian Hryvnia
    'USD': '$',    // US Dollar
    'VND': '₫',    // Vietnamese Dong
    'ZAR': 'R ',
};

// 查询历史低价包括的商店
const STORES = [
    "steam",
    // "amazonus",
    // "impulse",
    // "gamersgate",
    // "direct2drive",
    // "origin",
    // "uplay",
    // "indiegalastore",
    // "gamesplanet",
    // "indiegamestand",
    // "gog",
    // "nuuvem",
    // "dlgamer",
    // "humblestore",
    // "squenix",
    // "bundlestars",
    // "fireflower",
    // "humblewidgets",
    // "newegg",
    // "coinplay",
    // "wingamestore",
    // "macgamestore",
    // "gamebillet",
    // "silagames",
    // "itchio",
    // "gamejolt",
    // "paradox"
];

// 检查是否是愿望单页面
const isWishlist = location.href.includes("/wishlist/");
console.log('[史低]: 是愿望单页面?', isWishlist);

// 在app页、bundle页、sub页和愿望单页显示史低价格
let urlMatch = location.href.match(/(app|sub|bundle)\/(\d+)/);
let appId = "";
let type = "";
let subIds = [];
let bundleIds = [];
let appIds = [];
if (urlMatch && urlMatch.length == 3) {
    type = urlMatch[1];
    appId = urlMatch[2];
}

console.log('[史低]: 页面类型:', type, 'appId:', appId);

let cc = "cn";
if (CC_OVERRIDE.length > 0) {
    // 使用覆盖的货币区域
    cc = CC_OVERRIDE;
} else {
    // 使用默认的的货币区域
    let ccMatch = document.cookie.match(/steamCountry=([a-z]{2})/i);
    if (ccMatch !== null && ccMatch.length == 2) {
        cc = ccMatch[1];
    }
}
console.log('[史低]: 货币区域:', cc);

if (!isWishlist) {
    // 获取subs
    document.querySelectorAll("input[name=subid]")
        .forEach(sub => subIds.push(sub.value));
    // 获取bundles
    document.querySelectorAll("input[name=bundleid]")
        .forEach(sub => bundleIds.push(sub.value));
    console.log('[史低]: 非愿望单 - subIds:', subIds, 'bundleIds:', bundleIds);
    AddLowestPriceTag(appId, type, appIds, subIds, bundleIds, STORES.join(","), cc, location.protocol);
} else {
    type = "wishlist";
    initWishlist();
}

function initWishlist() {
    const rows = document.querySelectorAll('div.Panel[data-index]');
    if (rows.length === 0) {
        console.log('[史低]: 等待愿望单加载...');
        setTimeout(initWishlist, 500);
        return;
    }
    console.log('[史低]: 愿望单加载,处理...');
    appIds = [];
    subIds = [];
    bundleIds = [];
    rows.forEach(row => {
        const link = row.querySelector('a[href*="store.steampowered.com"]');
        if (link) {
            const match = link.href.match(/(app|sub|bundle)\/(\d+)/);
            if (match) {
                const itemType = match[1];
                const id = match[2];
                console.log('[史低]: 找到愿望单项 - 类型:', itemType, 'ID:', id);
                if (itemType === 'app') {
                    appIds.push(id);
                } else if (itemType === 'sub') {
                    subIds.push(id);
                } else if (itemType === 'bundle') {
                    bundleIds.push(id);
                }
            } else {
                console.log('[史低]: 愿望单项链接无匹配:', link.href);
            }
        } else {
            console.log('[史低]: 愿望单项无链接');
        }
    });
    console.log('[史低]: 愿望单 - appIds:', appIds, 'subIds:', subIds, 'bundleIds:', bundleIds);
    AddLowestPriceTag(appId, type, appIds, subIds, bundleIds, STORES.join(","), cc, location.protocol);
}

// 在商店页或愿望单添加史低信息
async function AddLowestPriceTag(mainAppId, pageType = "app", appIds = [], subIds = [], bundleIds = [], stores = "steam", cc = "cn", protocol = "https") {
    console.log('[史低]: AddLowestPriceTag 调用 - pageType:', pageType, 'mainAppId:', mainAppId, 'appIds:', appIds, 'subIds:', subIds, 'bundleIds:', bundleIds);
    // 史低信息容器们
    let lowestPriceNodes = {};
    let rowMap = {};
    let appInfosMap = {}; // To store info for re-adding on mutations

    if (pageType !== "wishlist") {
        // 统计subid
        let findSubIds = [];
        if (pageType == "bundle") {
            // bundle就一个, 视作subid
            findSubIds.push(mainAppId);
        } else if (pageType == "app" || pageType == "sub") {
            // app/sub/bundle 可能有好多
            findSubIds = subIds.slice();
            if (bundleIds.length > 0) {
                findSubIds.push.apply(findSubIds, bundleIds);
            }
        }
        console.log('[史低]: 非愿望单 - findSubIds:', findSubIds);

        // 寻找每一个subid的购买按钮,生成史低信息容器们
        findSubIds.forEach(subId => {
            let gameWrapper = null;
            try {
                gameWrapper = document.querySelector('.game_area_purchase_game input[value="' + subId + '"]');
                switch (INFO_STYLE) {
                    case 0:
                        gameWrapper = gameWrapper.parentNode.parentNode.querySelector('.game_purchase_action');
                        break;
                    case 1:
                        gameWrapper = gameWrapper.parentNode.parentNode;
                        break;
                }
            } catch (ex) {
                gameWrapper = null;
                console.log('[史低]: 寻找gameWrapper错误:', ex);
            }
            if (gameWrapper) {
                let lowestInfo = document.createElement("div");
                lowestInfo.className = "game_lowest_price";
                lowestInfo.innerText = "正在读取历史最低价格...";
                switch (INFO_STYLE) {
                    case 0:
                        gameWrapper.prepend(lowestInfo);
                        break;
                    case 1:
                        gameWrapper.append(lowestInfo);
                        break;
                }
                lowestPriceNodes[subId] = lowestInfo;
                console.log('[史低]: 为subId创建容器:', subId);
            } else {
                console.log('[史低]: 未找到gameWrapper for subId:', subId);
            }
        });
    } else {
        // 对于愿望单,收集row
        document.querySelectorAll('div.Panel[data-index]').forEach(row => {
            const link = row.querySelector('a[href*="store.steampowered.com"]');
            let id = "0";
            let itemType = "";
            if (link) {
                const match = link.href.match(/(app|sub|bundle)\/(\d+)/);
                if (match) {
                    itemType = match[1];
                    id = match[2];
                }
            }
            if (id !== "0") {
                rowMap[id + "_" + itemType] = row;
                console.log('[史低]: 收集愿望单row - 类型:', itemType, 'ID:', id);
            } else {
                console.log('[史低]: 愿望单项ID无效');
            }
        });
    }
    console.log('[史低]: rowMap:', rowMap);

    // 获取数据
    let data = null;
    try {
        data = await GettingSteamDBAppInfo(mainAppId, pageType, appIds, subIds, bundleIds, stores, cc, protocol);
        if ((typeof data == 'string')) {
            data = JSON.parse(data);
        }
        console.log('[史低]: 数据获取成功:', data);
    } catch (err) {
        console.log('[史低]: 数据获取错误:', err);
    }

    // 解析data
    let appInfos = [];
    if (pageType == "bundle") {
        if (data && data["bundle/" + mainAppId]) {
            appInfos.push({ Id: mainAppId, Type: "bundle", Info: data["bundle/" + mainAppId] });
        }
    } else {
        if (data && data.prices) {
            data = data.prices;
            for (let key in data) {
                let [itemType, appid] = key.split("/");
                if (!isNaN(appid)) {
                    appInfos.push({ Id: appid, Type: itemType, Info: data[key] });
                }
            }
        }
    }
    console.log('[史低]: 解析appInfos:', appInfos);

    // 如果查到info,塞到对应位置
    if (appInfos.length > 0) {
        // 为每一个添加史低
        appInfos.forEach(app => {
            let key = app.Id;
            if (pageType !== "wishlist") {
                key = app.Id;
            } else {
                key = app.Id + "_" + app.Type;
            }
            appInfosMap[key] = app; // Store for re-adding
            if (pageType !== "wishlist") {
                let lowestInfo = lowestPriceNodes[key];
                if (lowestInfo && app.Info && app.Info.lowest && app.Info.lowest.price) {
                    console.log('[史低]: 更新史低信息 - 类型:', app.Type, 'ID:', app.Id);
                    // 原有详细显示
                    lowestInfo.innerHTML =
                        // 历史最低
                        '历史最低价是 '
                        + '<span style="cursor:help;text-decoration:underline;" title="' + app.Info.lowest.recorded_formatted + '">'
                        + new Date(app.Info.lowest.timestamp).toLocaleDateString()
                        + '</span>&nbsp;时在&nbsp;'
                        + '<a target="_blank" href="' + app.Info.lowest.url + '"> '
                        + app.Info.lowest.shop.name
                        + '</a>&nbsp;中的&nbsp;'
                        + '<span class="discount_pct">-' + app.Info.lowest.cut + '%</span>'
                        + '<a target="_blank" title="查看价格历史" href="' + app.Info.urls.history + '"> '
                        + GETSymbol(app.Info.lowest.price.currency) + ' ' + app.Info.lowest.price.amount
                        + '</a>'

                        // 第二行
                        + '<br />'

                        // 当前最低
                        + (app.Info.current.price.amount <= app.Info.lowest.price.amount
                            ? '<span class="game_purchase_discount_countdown">当前为历史最低价</span>,'
                            : '<span>当前最低价是</span>')
                        + '在&nbsp;'
                        + '<a target="_blank" href="' + app.Info.current.url + '"> '
                        + app.Info.current.shop.name
                        + '</a>&nbsp;中的&nbsp;'
                        + '<span class="discount_pct">-' + app.Info.current.cut + '%</span>'
                        + '<a target="_blank" title="查看价格信息"  href="' + app.Info.urls.info + '"> '
                        + GETSymbol(app.Info.current.price.currency) + ' ' + app.Info.current.price.amount
                        + '</a>';
                } else if (lowestInfo) {
                    lowestInfo.innerHTML = "无史低数据";
                    console.log('[史低]: 无史低数据 - 类型:', app.Type, 'ID:', app.Id);
                }
            } else {
                addLowestToRow(key, appInfosMap);
            }
        });

        // For wishlist, set up MutationObserver to re-add on DOM changes
        if (pageType === "wishlist") {
            const observer = new MutationObserver((mutations) => {
                mutations.forEach((mutation) => {
                    if (mutation.type === 'childList' || mutation.type === 'subtree') {
                        Object.keys(rowMap).forEach(key => {
                            addLowestToRow(key, appInfosMap);
                        });
                    }
                });
            });
            observer.observe(document.body, { childList: true, subtree: true });
            console.log('[史低]: MutationObserver 已设置,用于处理React重新渲染');
        }
    } else {
        // metaInfo为空,或者appInfos无内容
        console.log('[史低]: get lowest price failed, data = %o', data);
        for (let id in lowestPriceNodes) {
            lowestPriceNodes[id].innerHTML = "";
        }
    }

    // 返回史低info
    return Promise.resolve(lowestPriceNodes);
}

// Helper to add/re-add lowest price to a wishlist row
function addLowestToRow(key, appInfosMap) {
    const app = appInfosMap[key];
    if (!app || !app.Info || !app.Info.lowest || !app.Info.lowest.price) return;

    const row = document.querySelector(`div.Panel[data-index] a[href*="/${app.Type}/${app.Id}"]`)?.closest('div.Panel[data-index]');
    if (!row) return;

    // Check if already added to avoid duplicates
    if (row.querySelector('.game_lowest_price')) return;

    console.log('[史低]: (Re)添加史低信息 - 类型:', app.Type, 'ID:', app.Id);
    let lowestInfo = document.createElement("div");
    lowestInfo.className = "game_lowest_price";
    lowestInfo.style = "font-size: 12px; color: #acf; margin-top: 5px; display: block; width: 100%;";
    // 愿望单简洁显示
    lowestInfo.innerHTML =
        '史低: ' + GETSymbol(app.Info.lowest.price.currency) + app.Info.lowest.price.amount +
        ' (-' + app.Info.lowest.cut + '%) 于 ' + new Date(app.Info.lowest.timestamp).toLocaleDateString();

    const container = row.querySelector('div._7zQ9up20PmA-');
    if (container) {
        container.appendChild(lowestInfo);
        console.log('[史低]: 附加到容器 class: _7zQ9up20PmA-');
    } else {
        row.appendChild(lowestInfo);
        console.log('[史低]: 附加到row');
    }
}

function GETSymbol(currency) {
    return currency in CURRENCY_SYMBOLS ? CURRENCY_SYMBOLS[currency] : currency;
}
// 获取史低信息
async function GettingSteamDBAppInfo(mainAppId, pageType = "app", appIds = [], subIds = [], bundleIds = [], stores = "steam", cc = "cn", protocol = "https") {
    console.log('[史低]: GettingSteamDBAppInfo 调用 - pageType:', pageType, 'mainAppId:', mainAppId, 'appIds:', appIds, 'subIds:', subIds, 'bundleIds:', bundleIds);
    let requestPromise = null;

    if (pageType !== "wishlist" && !isNaN(mainAppId) && parseInt(mainAppId) > 0) {
        if (pageType == "bundle") {
            bundleIds = [mainAppId];
            appIds = [];
            subIds = [];
        } else if (pageType == "app") {
            appIds = [mainAppId];
            // subIds and bundleIds already collected
        } else if (pageType == "sub") {
            subIds = [mainAppId];
            // bundleIds already collected
            appIds = [];
        }
    }

    let requestUrl = protocol + "//api.augmentedsteam.com/prices/v2";
    console.log('[史低]: 请求URL:', requestUrl);
    const requestData = {
        "country": cc,
        "apps": appIds.map(x => parseInt(x)).filter(x => !isNaN(x)),
        "subs": subIds.map(x => parseInt(x)).filter(x => !isNaN(x)),
        "bundles": bundleIds.map(x => parseInt(x)).filter(x => !isNaN(x)),
        "voucher": true,
        "shops": [61]
    };
    console.log('[史低]: 请求数据:', requestData);
    //shops  61 = steam
    requestPromise = new Promise((resolve, reject) => {
        GM_xmlhttpRequest({
            method: "POST",
            headers: { 'Content-Type': 'application/json' },
            url: requestUrl,
            data: JSON.stringify(requestData),
            onload: function (response) {
                console.log('[史低]: 请求成功 - 响应:', response.response);
                resolve(response.response);
            },
            onerror: function (error) {
                console.log('[史低]: 请求错误:', error);
                reject(error);
            }
        });
    });

    return requestPromise;
}