Steam Inventory Auto Sell Script

Automatically list items in your Steam inventory.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Steam Inventory Auto Sell Script
// @description  Automatically list items in your Steam inventory.
// @version      1.0.0
// @author       RLAlpha49
// @namespace    https://github.com/RLAlpha49/Steam-Inventory-Auto-Sell-Script
// @license      MIT
// @match        https://steamcommunity.com/id/*/inventory*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Set to true to enable debug logging
    const DEBUG = false;
    function log(...args) {
        // User-facing info logs (not too many)
        console.log('[Steam Auto Sell Helper]', ...args);
    }
    function debug(...args) {
        if (DEBUG) console.debug('[Steam Auto Sell Helper][DEBUG]', ...args);
    }

    function isOwnInventory() {
        // Get account pulldown text
        const accountPulldown = document.getElementById('account_pulldown');
        if (!accountPulldown) return false;
        const accountName = accountPulldown.textContent.trim();

        // Get persona name text
        const personaNameElem = document.querySelector('.whiteLink.persona_name_text_content');
        if (!personaNameElem) return false;
        const personaName = personaNameElem.textContent.trim();

        // Compare
        return accountName === personaName;
    }

    function addStartStopButton() {
        if (document.getElementById('my-userscript-toggle-btn')) return; // Prevent duplicate
        const btn = document.createElement('button');
        btn.id = 'my-userscript-toggle-btn';
        btn.textContent = 'Start Script';
        btn.style.float = 'right';
        btn.style.margin = '8px';
        btn.style.zIndex = 10000;
        let running = false;
        let stopRequested = false;

        async function waitForMarketableInput(timeout = 5000) {
            const start = Date.now();
            while (Date.now() - start < timeout) {
                const input = document.querySelector('input[id*="misc_marketable"]');
                if (input) return input;
                await new Promise(res => setTimeout(res, 100));
            }
            return null;
        }

        async function waitForPrice(marketActionsDiv, visibleIndex, link, maxRetries = 3, itemInfoDiv = null) {
            let retries = 0;
            const waitTime = 15000; // 15 seconds
            while (retries < maxRetries && !stopRequested) {
                // Only check price once per retry loop
                if (retries > 0) {
                    // Re-click the itemHolder link to reload the item info
                    if (link) {
                        link.click();
                    }
                    // Wait 1 second after click before checking price
                    await new Promise(res => setTimeout(res, 1000));
                    // Re-select the visible iteminfo div and marketActionsDiv after waiting
                    let itemInfo0 = document.getElementById('iteminfo0');
                    let itemInfo1 = document.getElementById('iteminfo1');
                    if (itemInfo0 && itemInfo0.style.display !== 'none') {
                        itemInfoDiv = itemInfo0;
                    } else if (itemInfo1 && itemInfo1.style.display !== 'none') {
                        itemInfoDiv = itemInfo1;
                    }
                    marketActionsDiv = null;
                    if (itemInfoDiv) {
                        marketActionsDiv = itemInfoDiv.querySelector('#iteminfo0_item_market_actions, #iteminfo1_item_market_actions, .item_market_actions');
                    }
                }
                if (marketActionsDiv) {
                    const priceDivs = marketActionsDiv.querySelectorAll('div');
                    for (const div of priceDivs) {
                        if (div.textContent.includes('Starting at:')) {
                            const match = div.textContent.match(/Starting at:\s*([$€£]?\d+[.,]?\d*)/);
                            if (match) {
                                const price = match[1];
                                log(`Found price for visible itemHolder #${visibleIndex}: ${price}`);
                                return price;
                            }
                        }
                    }
                }
                // On the first failure, check for the alternative sell button
                // The alternative sell button (a.btn_small.btn_darkblue_white_innerfade) is added by the SteamDB browser extension 'SteamDB Quick Sell'.
                // It is used as a fallback if the steam website is rate limiting displaying the list price for items.
                if (retries === 0 && itemInfoDiv) {
                    // Wait half a second before trying to find the alternative sell button
                    await new Promise(res => setTimeout(res, 500));
                    const altSellBtn = itemInfoDiv.querySelector('a.btn_small.btn_darkblue_white_innerfade');
                    if (altSellBtn) {
                        // If present, wait up to 10s for it to become enabled
                        let waited = 0;
                        const maxWait = 10000; // 10 seconds
                        const interval = 250;
                        while (altSellBtn.classList.contains('disabled') && waited < maxWait && !stopRequested) {
                            await new Promise(res => setTimeout(res, interval));
                            waited += interval;
                        }
                        if (!altSellBtn.classList.contains('disabled')) {
                            log(`Clicking alternate sell button for visible itemHolder #${visibleIndex} (early fallback)`);
                            let modalAppeared = false;
                            for (let attempt = 1; attempt <= 3; attempt++) {
                                altSellBtn.click();
                                debug(`Clicked alternate sell button (attempt ${attempt})`);
                                // Wait up to 1 second for the modal to appear
                                waited = 0;
                                while (waited < 1000) {
                                    const modal = document.getElementById('market_sell_dialog');
                                    if (modal && modal.style.display !== 'none') {
                                        modalAppeared = true;
                                        break;
                                    }
                                    await new Promise(res => setTimeout(res, 100));
                                    waited += 100;
                                }
                                if (modalAppeared) break;
                            }
                            if (!modalAppeared) {
                                log('Error: Alternative sell modal did not appear after 3 attempts. Skipping item.');
                                return;
                            }
                            // Skip price input, proceed with SSA and accept
                            const ssaCheckbox = document.getElementById('market_sell_dialog_accept_ssa');
                            if (ssaCheckbox && !ssaCheckbox.checked) {
                                ssaCheckbox.click();
                                debug('Checked SSA checkbox.');
                            }
                            const acceptBtn = document.getElementById('market_sell_dialog_accept');
                            if (acceptBtn) {
                                acceptBtn.click();
                                debug('Clicked accept button.');
                                await new Promise(res => setTimeout(res, 500));
                            } else {
                                log('Accept button not found (early fallback).');
                            }
                            const okBtn = document.getElementById('market_sell_dialog_ok');
                            if (okBtn) {
                                okBtn.click();
                                debug('Clicked OK button.');
                                await new Promise(res => setTimeout(res, 500));
                                // If there is an error, close the modal manually
                                const errorDiv = document.getElementById('market_sell_dialog_error');
                                if (errorDiv && errorDiv.style.display !== 'none') {
                                    if (errorDiv.textContent && errorDiv.textContent.includes('You have too many listings pending confirmation.')) {
                                        log('Too many listings pending confirmation. Stopping script.');
                                        stopRequested = true;
                                        return;
                                    }
                                    const closeBtn = document.querySelector('.newmodal_close');
                                    if (closeBtn) {
                                        closeBtn.click();
                                        log('Closed modal manually due to error after OK click (early fallback).');
                                    } else {
                                        log('Could not find .newmodal_close to close modal after error (early fallback).');
                                    }
                                }
                                // Wait until the modal background is hidden before continuing
                                let modalWaitTries = 0;
                                const maxModalWaitTries = 20; // 20 * 250ms = 5s max
                                while (modalWaitTries < maxModalWaitTries * 2) {
                                    const modalBg = document.querySelector('.newmodal_background');
                                    if (!modalBg || modalBg.style.display === 'none') {
                                        break;
                                    }
                                    await new Promise(res => setTimeout(res, 250));
                                    modalWaitTries++;
                                }
                                if (modalWaitTries >= maxModalWaitTries) {
                                    // Try to close the modal manually if still open
                                    const closeBtn = document.querySelector('.newmodal_close');
                                    if (closeBtn) {
                                        closeBtn.click();
                                        log('Modal background did not hide after OK click (early fallback, timeout). Closed modal manually.');
                                    } else {
                                        log('Modal background did not hide after OK click (early fallback, timeout). Could not find .newmodal_close to close modal manually.');
                                    }
                                } else {
                                    log('Modal background hidden, continuing to next item (early fallback).');
                                }
                            } else {
                                log('OK button not found (early fallback).');
                            }
                            // Return a special value to indicate fallback was used
                            return '__FALLBACK_USED__';
                        }
                        // If still disabled after waiting, continue to retries
                    }
                }
                retries++;
                if (retries < maxRetries) {
                    log(`Price not found for visible itemHolder #${visibleIndex}, retrying in ${waitTime / 1000}s (retry #${retries} of ${maxRetries})...`);
                    await new Promise(res => setTimeout(res, waitTime - 1000));
                }
            }
            return null;
        }

        // Simulate real typing with keyboard events
        async function simulateTyping(input, text) {
            input.value = '';
            for (const char of text) {
                const eventOptions = { bubbles: true, cancelable: true, key: char, char, keyCode: char.charCodeAt(0) };
                input.dispatchEvent(new KeyboardEvent('keydown', eventOptions));
                input.dispatchEvent(new KeyboardEvent('keypress', eventOptions));
                input.value += char;
                input.dispatchEvent(new Event('input', { bubbles: true }));
                input.dispatchEvent(new KeyboardEvent('keyup', eventOptions));
                await new Promise(res => setTimeout(res, 50));
            }
            input.dispatchEvent(new Event('change', { bubbles: true }));
        }

        async function clickItemHolders(inventoryPage) {
            if (!inventoryPage) {
                log('No inventory_page found for current page!');
                return;
            }
            const itemHolders = inventoryPage.querySelectorAll('.itemHolder');
            log(`Found ${itemHolders.length} .itemHolder elements.`);
            let visibleIndex = 1;
            for (let i = 0; i < itemHolders.length && visibleIndex <= 25; i++) {
                if (stopRequested) {
                    log('Stop requested. Halting immediately.');
                    break;
                }
                const itemHolder = itemHolders[i];
                if (itemHolder.style.display === 'none') {
                    debug(`Skipping itemHolder at DOM index ${i} (display: none)`);
                    continue;
                }
                const link = itemHolder.querySelector('a.inventory_item_link');
                if (link) {
                    debug(`Clicking inventory_item_link in visible itemHolder #${visibleIndex} (DOM index ${i})`);
                    link.click();
                    // Wait for item info to update
                    await new Promise(res => setTimeout(res, 500));
                    // Dynamically select the visible iteminfo div
                    let itemInfoDiv = null;
                    const itemInfo0 = document.getElementById('iteminfo0');
                    const itemInfo1 = document.getElementById('iteminfo1');
                    if (itemInfo0 && itemInfo0.style.display !== 'none') {
                        itemInfoDiv = itemInfo0;
                    } else if (itemInfo1 && itemInfo1.style.display !== 'none') {
                        itemInfoDiv = itemInfo1;
                    }
                    let marketActionsDiv = null;
                    if (itemInfoDiv) {
                        marketActionsDiv = itemInfoDiv.querySelector('#iteminfo0_item_market_actions, #iteminfo1_item_market_actions, .item_market_actions');
                    }
                    const price = await waitForPrice(marketActionsDiv, visibleIndex, link, 3, itemInfoDiv);
                    if (stopRequested) {
                        log('Stop requested during price wait. Halting immediately.');
                        break;
                    }
                    if (!price || price === '__FALLBACK_USED__') {
                        if (!price) {
                            log(`No 'Starting at:' price found for visible itemHolder #${visibleIndex} after retries, attempting fallback.`);
                        }
                        visibleIndex++;
                        continue;
                    }
                    // After finding the price, click the green market action button
                    if (itemInfoDiv) {
                        const sellBtn = itemInfoDiv.querySelector('a.item_market_action_button.item_market_action_button_green');
                        if (sellBtn) {
                            debug(`Clicking green market action button for visible itemHolder #${visibleIndex}`);
                            sellBtn.click();
                            // Wait 1 second for the dialog to appear
                            await new Promise(res => setTimeout(res, 1000));
                            // Set the price in the input
                            const priceInput = document.getElementById('market_sell_buyercurrency_input');
                            if (priceInput) {
                                await simulateTyping(priceInput, price);
                                await new Promise(res => setTimeout(res, 100));
                                debug(`Simulated typing price input: ${price} (with keyboard events)`);
                            } else {
                                log('Price input not found.');
                            }
                            // Check the SSA checkbox
                            const ssaCheckbox = document.getElementById('market_sell_dialog_accept_ssa');
                            if (ssaCheckbox && !ssaCheckbox.checked) {
                                ssaCheckbox.click();
                                debug('Checked SSA checkbox.');
                            }
                            // Click the accept button
                            const acceptBtn = document.getElementById('market_sell_dialog_accept');
                            if (acceptBtn) {
                                acceptBtn.click();
                                debug('Clicked accept button.');
                                // Wait 0.5 second after clicking accept
                                await new Promise(res => setTimeout(res, 500));
                            } else {
                                log('Accept button not found.');
                            }
                            // Click the OK button
                            const okBtn = document.getElementById('market_sell_dialog_ok');
                            if (okBtn) {
                                okBtn.click();
                                debug('Clicked OK button.');
                                // If there is an error, close the modal manually
                                const errorDiv = document.getElementById('market_sell_dialog_error');
                                if (errorDiv && errorDiv.style.display !== 'none') {
                                    if (errorDiv.textContent && errorDiv.textContent.includes('You have too many listings pending confirmation.')) {
                                        log('Too many listings pending confirmation. Stopping script.');
                                        stopRequested = true;
                                        return;
                                    }
                                    const closeBtn = document.querySelector('.newmodal_close');
                                    if (closeBtn) {
                                        closeBtn.click();
                                        log('Closed modal manually due to error after OK click (early fallback).');
                                    } else {
                                        log('Could not find .newmodal_close to close modal after error (early fallback).');
                                    }
                                }
                                // Wait until the modal background is hidden before continuing
                                let modalWaitTries = 0;
                                const maxModalWaitTries = 40; // 40 * 250ms = 10s max
                                while (modalWaitTries < maxModalWaitTries) {
                                    const modalBg = document.querySelector('.newmodal_background');
                                    if (!modalBg || modalBg.style.display === 'none') {
                                        break;
                                    }
                                    await new Promise(res => setTimeout(res, 250));
                                    modalWaitTries++;
                                }
                                if (modalWaitTries >= maxModalWaitTries) {
                                    log('Modal background did not hide after OK click (timeout).');
                                } else {
                                    debug('Modal background hidden, continuing to next item.');
                                }
                            } else {
                                log('OK button not found.');
                            }
                        } else {
                            log(`No green market action button found for visible itemHolder #${visibleIndex}`);
                        }
                    }
                } else {
                    log(`No inventory_item_link found in visible itemHolder #${visibleIndex} (DOM index ${i})`);
                }
                await new Promise(res => setTimeout(res, 1000));
                visibleIndex++;
            }
            log('Item click sequence complete.');
        }

        async function processAllPages() {
            let page = 1;
            // Ensure the filter tag is shown and marketable filter is checked only once at the start
            log('Ensuring filters are set before starting page processing...');
            const filterTagCtn = document.querySelector('.filter_tag_button_ctn');
            if (filterTagCtn) {
                const showBtn = filterTagCtn.querySelector('#filter_tag_show');
                const hideBtn = filterTagCtn.querySelector('#filter_tag_hide');
                if (showBtn && hideBtn) {
                    if (showBtn.style.display !== 'none') {
                        debug('Clicking filter_tag_show to reveal filters...');
                        showBtn.click();
                        // Wait for marketable input to appear
                        debug('Waiting for marketable filter input to appear...');
                        const marketableInput = await waitForMarketableInput();
                        if (marketableInput) {
                            debug('Marketable filter input appeared.');
                        } else {
                            log('Timed out waiting for marketable filter input.');
                        }
                    } else {
                        debug('filter_tag_show is hidden, filters already visible.');
                    }
                } else {
                    log('filter_tag_show or filter_tag_hide not found in filter_tag_button_ctn.');
                }
            } else {
                log('No filter_tag_button_ctn found.');
            }
            // Ensure the marketable filter is checked
            const marketableInput = document.querySelector('input[id*="misc_marketable"]');
            if (marketableInput) {
                if (!marketableInput.checked) {
                    debug('Checking the marketable filter input...');
                    marketableInput.click();
                    await new Promise(res => setTimeout(res, 2000));
                    debug('Waited 2 seconds after checking marketable filter.');
                } else {
                    debug('Marketable filter already checked.');
                }
            } else {
                log('No marketable filter input found.');
            }
            while (true) {
                log(`Processing page ${page}...`);
                // Re-query the current page index and inventory_page each time
                const pageCurSpan = document.getElementById('pagecontrol_cur');
                let inventoryPage = null;
                if (pageCurSpan) {
                    const pageIndex = parseInt(pageCurSpan.textContent.trim(), 10);
                    const allInventoryPages = document.querySelectorAll('.inventory_page');
                    if (pageIndex >= 0 && pageIndex < allInventoryPages.length) {
                        inventoryPage = allInventoryPages[pageIndex];
                        debug(`Using inventory_page at index ${pageIndex}.`);
                    } else {
                        log(`Invalid page index: ${pageIndex}.`);
                    }
                } else {
                    log('No pagecontrol_cur span found.');
                }
                await clickItemHolders(inventoryPage);
                if (stopRequested) {
                    log('Stop requested. Stopping immediately.');
                    break;
                }
                const nextBtn = document.getElementById('pagebtn_next');
                if (nextBtn && !nextBtn.classList.contains('disabled')) {
                    debug('Clicking next page button...');
                    nextBtn.click();
                    debug('Waiting 1.5 seconds for next page to load and styles to update...');
                    await new Promise(res => setTimeout(res, 1500));
                    page++;
                } else {
                    log('No next page or next page button is disabled. Stopping.');
                    break;
                }
            }
        }

        btn.onclick = async function() {
            running = !running;
            btn.textContent = running ? 'Stop Script' : 'Start Script';
            if (running) {
                stopRequested = false;
                log('Script started.');
                await processAllPages();
                running = false;
                btn.textContent = 'Start Script';
                log('Script finished.');
            } else {
                stopRequested = true;
                log('Script stopped by user.');
            }
        };
        const logosDiv = document.getElementById('inventory_logos');
        if (logosDiv) {
            logosDiv.appendChild(btn);
        } else {
            // fallback: add to body if not found
        document.body.appendChild(btn);
        }
    }

    function main() {
        if (isOwnInventory()) {
            addStartStopButton();
        }
    }

    // Wait for DOM to be ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', main);
    } else {
        main();
    }
})();