您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Auto-exports Steam games list with WebSocket login automation support
- // ==UserScript==
- // @name Steam Games Export with WebSocket
- // @namespace steamutils
- // @version 0.7.4
- // @description Auto-exports Steam games list with WebSocket login automation support
- // @author mustafachyi
- // @match *://steamcommunity.com/*
- // @grant GM_cookie
- // @grant GM_xmlhttpRequest
- // @grant GM_download
- // @connect steamcommunity.com
- // @connect localhost
- // @connect 127.0.0.1
- // @run-at document-start
- // ==/UserScript==
- (() => {
- 'use strict';
- // Configuration
- const CONFIG = {
- urls: {
- base: 'https://steamcommunity.com',
- login: '/login/home/',
- games: '/games'
- },
- storage: {
- profiles: 'steam_exported_profiles',
- username: 'steam_last_username',
- mode: 'steam_export_mode'
- },
- retry: { max: 20, delay: 500, loginCheck: 1500 },
- ui: { notifyDuration: 3000, animDuration: 300 },
- keys: { logout: { ctrl: true, alt: true, key: 'l' } },
- ws: { url: 'ws://127.0.0.1:27060', fallback: true }
- };
- // URL utilities
- const url = {
- isLogin: () => location.href.includes('/login/home'),
- isGames: () => location.pathname.includes('/games'),
- isProfile: () => /\/(?:id|profiles)\/[^\/]+(?:\/home|\/?$)/.test(location.pathname),
- isFamilyPin: () => location.href.includes('/my/goto'),
- getBase: () => (location.href.match(/(.*\/(?:id|profiles)\/[^\/]+)(?:\/home)?/) || [])[1] || null,
- getSteamId: () => {
- const match = location.pathname.match(/\/(?:id|profiles)\/([^\/]+)(?:\/home)?/);
- return match ? match[1] : null;
- },
- resolveVanityURL: async (vanityURL) => {
- return utils.request(`https://steamcommunity.com/id/${vanityURL}?xml=1`, {
- parser: res => {
- const steamID64 = res.responseText.match(/<steamID64>(\d+)<\/steamID64>/);
- return steamID64 ? steamID64[1] : null;
- }
- });
- }
- };
- // Early URL handling
- if (location.href === `${CONFIG.urls.base}/` || location.href === CONFIG.urls.base) {
- location.replace(`${CONFIG.urls.base}${CONFIG.urls.login}`);
- return;
- }
- const profileMatch = location.pathname.match(/^\/(id|profiles)\/([^\/]+)(?:\/(?:home)?)?$/);
- if (profileMatch) {
- const [, type, id] = profileMatch;
- try {
- const profiles = JSON.parse(localStorage.getItem(CONFIG.storage.profiles) || '{}');
- if (profiles[id] === undefined) {
- location.replace(`${CONFIG.urls.base}/${type}/${id}${CONFIG.urls.games}`);
- return;
- }
- } catch {}
- }
- // Utilities
- const utils = {
- setReactValue(input, value) {
- Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set.call(input, value);
- input.dispatchEvent(new Event('input', { bubbles: true }));
- input.dispatchEvent(new Event('change', { bubbles: true }));
- },
- async handleLogin(loginButton) {
- const [userInput, passInput] = document.querySelectorAll('input._2GBWeup5cttgbTw8FM3tfx');
- if (userInput && passInput && loginButton) {
- try {
- loginButton.click();
- setTimeout(() => {
- if (url.isLogin()) ws.send({ type: 'login_failed' });
- }, CONFIG.retry.loginCheck);
- return true;
- } catch (e) {
- console.log('Login error:', e);
- }
- }
- const form = document.querySelector('form._2v60tM463fW0V7GDe92E5f');
- if (form) {
- try {
- form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
- setTimeout(() => {
- if (url.isLogin()) ws.send({ type: 'login_failed' });
- }, CONFIG.retry.loginCheck);
- return true;
- } catch (e) {
- console.log('Form submission error:', e);
- }
- }
- return false;
- },
- request: (url, { method = 'GET', parser } = {}) => new Promise(resolve =>
- GM_xmlhttpRequest({
- method,
- url,
- onload: res => resolve(parser ? parser(res) : res),
- onerror: () => resolve(null)
- })
- )
- };
- // WebSocket handler
- const ws = {
- conn: null,
- connected: false,
- mode: localStorage.getItem(CONFIG.storage.mode) || 'manual',
- connect() {
- if (this.conn?.readyState <= WebSocket.OPEN) return;
- try {
- this.conn = new WebSocket(CONFIG.ws.url);
- this.conn.onopen = () => {
- this.send({ type: 'identify', client: 'userscript', version: '0.7.4' });
- this.connected = true;
- };
- this.conn.onmessage = ({ data }) => {
- try {
- const msg = JSON.parse(data);
- const handler = this.handlers[msg.type];
- handler && handler(msg);
- } catch {}
- };
- this.conn.onclose = this.conn.onerror = () => {
- this.cleanup();
- if (CONFIG.ws.fallback && url.isLogin()) this.fallback = true;
- };
- } catch {
- this.cleanup();
- if (CONFIG.ws.fallback && url.isLogin()) this.fallback = true;
- }
- },
- handlers: {
- connected(msg) {
- if (ws.mode === msg.mode) return;
- ws.mode = msg.mode;
- localStorage.setItem(CONFIG.storage.mode, ws.mode);
- ui.notify('Connected', `Server connected in ${ws.mode} mode`);
- },
- manual_mode() {
- if (ws.mode === 'manual') return;
- ws.mode = 'manual';
- localStorage.setItem(CONFIG.storage.mode, 'manual');
- ui.notify('Mode Changed', 'Switched to manual mode');
- },
- account_data(msg) {
- const [user, pass] = msg.credentials.split(':').map(s => s.trim());
- auth.fillCredentials(user, pass);
- },
- all_done() {
- ui.notify('Complete', 'All accounts have been processed');
- }
- },
- send(data) {
- return this.conn?.readyState === WebSocket.OPEN && this.conn.send(JSON.stringify(data));
- },
- cleanup() {
- if (this.conn) {
- this.conn.close();
- this.conn = null;
- this.connected = false;
- }
- }
- };
- // UI components
- const ui = {
- styles: `
- .steam_export_notification{position:fixed;bottom:20px;right:20px;background:#1b2838;border:1px solid #66c0f4;color:#fff;padding:15px;border-radius:3px;box-shadow:0 0 10px rgba(0,0,0,.5);z-index:9999;font-family:"Motiva Sans",Arial,sans-serif;animation:steamNotificationSlide .3s ease-out;display:flex;align-items:center;gap:10px;min-width:280px}
- .steam_export_notification .icon{width:24px;height:24px;background:#66c0f4;border-radius:3px;display:flex;align-items:center;justify-content:center}
- .steam_export_notification .content{flex-grow:1}
- .steam_export_notification .title{font-weight:700;margin-bottom:3px;color:#66c0f4}
- .steam_export_notification .message{font-size:12px;color:#acb2b8}
- @keyframes steamNotificationSlide{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}
- .steam_export_btn{display:inline-flex;align-items:center;padding:0 15px;line-height:24px;border-radius:2px;background:#101822;color:#fff;margin-left:10px;cursor:pointer;border:none;font-family:"Motiva Sans",Arial,sans-serif;transition:all .25s ease}
- .steam_export_btn:hover{background:#4e92b9}
- body.login .responsive_page_frame{height:100vh!important;overflow:hidden!important;display:flex!important;flex-direction:column!important}
- body.login .responsive_page_content{flex:1!important;overflow:hidden!important;display:flex!important;flex-direction:column!important}
- body.login .responsive_page_template_content{flex:1!important;display:flex!important;flex-direction:column!important;justify-content:center!important;align-items:center!important;min-height:0!important}
- body.login .page_content{margin:0!important;padding:0 16px!important;width:100%!important;max-width:740px!important;box-sizing:border-box!important}
- body.login #footer,body.login #global_header,body.login .responsive_header{position:relative!important}
- body.login #footer{margin-top:auto!important;padding:16px 0!important}
- body.login #footer_spacer{display:none!important}
- body.login #global_header{padding:16px 0!important}
- body.login .responsive_header{padding:12px 0!important}
- body.login .login_bottom_row{margin:16px 0!important}
- body.login [data-featuretarget="login"]{margin:0!important;padding:16px!important;background:rgba(0,0,0,0.2)!important;border-radius:4px!important;box-shadow:0 0 10px rgba(0,0,0,0.3)!important}
- `.replace(/\s+/g, ' '),
- init() {
- const style = document.createElement('style');
- style.textContent = this.styles;
- (document.head || document.documentElement).appendChild(style);
- },
- notify(title, message) {
- const el = document.createElement('div');
- el.className = 'steam_export_notification';
- el.innerHTML = `<div class="icon">✓</div><div class="content"><div class="title">${title}</div><div class="message">${message}</div></div>`;
- document.body.appendChild(el);
- setTimeout(() => {
- el.style.animation = 'steamNotificationSlide 0.3s ease-in reverse';
- setTimeout(() => el.remove(), CONFIG.ui.animDuration);
- }, CONFIG.ui.notifyDuration);
- },
- addExportButton() {
- const btn = document.createElement('a');
- btn.className = 'steam_export_btn';
- btn.textContent = 'Export Games';
- btn.onclick = () => games.exportFromConfig();
- const observer = new MutationObserver((_, obs) => {
- const header = document.querySelector('.profile_small_header_text');
- if (header) {
- header.appendChild(btn);
- obs.disconnect();
- }
- });
- observer.observe(document.documentElement, { childList: true, subtree: true });
- }
- };
- // Storage management
- const storage = {
- get(key) {
- try {
- return JSON.parse(localStorage.getItem(key) || '{}');
- } catch {
- return {};
- }
- },
- set(key, value) {
- localStorage.setItem(key, JSON.stringify(value));
- },
- markExported(steamId, count) {
- const profiles = this.get(CONFIG.storage.profiles);
- profiles[steamId] = count;
- this.set(CONFIG.storage.profiles, profiles);
- },
- shouldExport(steamId, count) {
- const profiles = this.get(CONFIG.storage.profiles);
- return profiles[steamId] === undefined || profiles[steamId] !== count;
- }
- };
- // Games management
- const games = {
- async waitForData(retries = 0) {
- const config = document.getElementById('gameslist_config')?.dataset.profileGameslist;
- if (!config) {
- if (retries < CONFIG.retry.max) {
- setTimeout(() => this.waitForData(retries + 1), CONFIG.retry.delay);
- }
- return;
- }
- try {
- const data = JSON.parse(config);
- if (!data.rgGames?.length) return;
- if (storage.shouldExport(data.strSteamId, data.rgGames.length)) {
- await this.export(data, true);
- storage.markExported(data.strSteamId, data.rgGames.length);
- }
- !document.querySelector('.steam_export_btn') && ui.addExportButton();
- } catch {}
- },
- async exportFromConfig() {
- const config = document.getElementById('gameslist_config')?.dataset.profileGameslist;
- if (!config) return;
- try {
- const data = JSON.parse(config);
- if (data.rgGames?.length) {
- await this.export(data);
- storage.markExported(data.strSteamId, data.rgGames.length);
- }
- } catch {}
- },
- async export(data, isAutoExport = false) {
- const username = localStorage.getItem(CONFIG.storage.username) || data.strProfileName || 'unknown';
- const content = data.rgGames.map(g => g.name).join('\n');
- const url = URL.createObjectURL(new Blob([content], { type: 'text/plain' }));
- const cleanup = setTimeout(() => URL.revokeObjectURL(url), 30000);
- try {
- await new Promise((resolve, reject) => {
- GM_download({
- url,
- name: `steam_games/${username}_games.txt`,
- saveAs: false,
- onload: resolve,
- onerror: reject
- });
- });
- ui.notify('Games List Exported', `Saved ${data.rgGames.length} games to steam_games/${username}_games.txt`);
- isAutoExport && typeof Logout === 'function' && setTimeout(Logout, 1000);
- } catch (error) {
- if (error.includes('No such file or directory')) {
- await new Promise(resolve => {
- GM_download({
- url: 'data:text/plain;base64,',
- name: 'steam_games/.folder',
- saveAs: false,
- onload: resolve
- });
- });
- return this.export(data, isAutoExport);
- }
- ui.notify('Export Failed', 'Could not save games list. Please try again.');
- } finally {
- clearTimeout(cleanup);
- URL.revokeObjectURL(url);
- }
- },
- async resolveID(idOrVanity) {
- if (/^\d+$/.test(idOrVanity)) return idOrVanity;
- return await url.resolveVanityURL(idOrVanity) || idOrVanity;
- },
- async getCount(idOrVanity) {
- const resolvedID = await this.resolveID(idOrVanity);
- const urlPath = /^\d+$/.test(resolvedID) ? `profiles/${resolvedID}` : `id/${resolvedID}`;
- return utils.request(`${CONFIG.urls.base}/${urlPath}/games`, {
- parser: res => {
- const doc = new DOMParser().parseFromString(res.responseText, 'text/html');
- const data = JSON.parse(doc.getElementById('gameslist_config')?.dataset.profileGameslist || '{}');
- return data.rgGames?.length || null;
- }
- });
- }
- };
- // Authentication
- const auth = {
- setupLoginCapture() {
- const observer = new MutationObserver((_, obs) => {
- const form = document.querySelector('form._2v60tM463fW0V7GDe92E5f');
- if (!form) return;
- const [userInput, passInput] = form.querySelectorAll('input._2GBWeup5cttgbTw8FM3tfx');
- if (!userInput || !passInput) return;
- const loginButton = form.querySelector('button.DjSvCZoKKfoNSmarsEcTS');
- if (!loginButton) return;
- userInput.addEventListener('paste', e => {
- const text = (e.clipboardData || window.clipboardData).getData('text');
- if (!text.includes(':')) return;
- e.preventDefault();
- const [user, pass] = text.split(':').map(s => s.trim());
- this.fillCredentials(user, pass);
- });
- if (!ws.fallback) {
- ws.connect();
- setTimeout(() => ws.send({ type: 'ready_for_login' }), 500);
- }
- loginButton.addEventListener('click', () => {
- const username = userInput.value.trim();
- username && localStorage.setItem(CONFIG.storage.username, username);
- });
- obs.disconnect();
- });
- observer.observe(document.documentElement, { childList: true, subtree: true });
- },
- fillCredentials(user, pass) {
- if (!user || !pass) return;
- const form = document.querySelector('form._2v60tM463fW0V7GDe92E5f');
- if (!form) return;
- const [userInput, passInput] = form.querySelectorAll('input._2GBWeup5cttgbTw8FM3tfx');
- const loginButton = form.querySelector('button.DjSvCZoKKfoNSmarsEcTS');
- if (!userInput || !passInput || !loginButton) return;
- utils.setReactValue(userInput, user);
- utils.setReactValue(passInput, pass);
- localStorage.setItem(CONFIG.storage.username, user);
- ui.notify('Credentials Filled', 'Username and password have been entered');
- ws.connected && setTimeout(() => {
- ws.send({ type: 'credentials_filled' });
- utils.handleLogin(loginButton);
- }, 1000);
- },
- async checkState() {
- return utils.request(`${CONFIG.urls.base}/my/`, {
- parser: res => !res.finalUrl.includes('/login')
- });
- },
- setupLogout() {
- document.addEventListener('keydown', e => {
- const { ctrl, alt, key } = CONFIG.keys.logout;
- if ((!ctrl || e.ctrlKey) && (!alt || e.altKey) && e.key.toLowerCase() === key) {
- e.preventDefault();
- typeof Logout === 'function' && Logout();
- }
- }, true);
- }
- };
- // Login page optimization
- if (location.href.includes('/login/home')) {
- const blockStyle = document.createElement('style');
- blockStyle.textContent = `#footer,#global_header,.login_bottom_row{display:none!important;visibility:hidden!important;opacity:0!important;pointer-events:none!important;position:absolute!important;width:0!important;height:0!important;overflow:hidden!important;clip:rect(0,0,0,0)!important}`;
- document.documentElement.appendChild(blockStyle);
- const observer = new MutationObserver(mutations => {
- for (const { addedNodes } of mutations) {
- for (const node of addedNodes) {
- if (node.nodeType !== 1) continue;
- if (node.matches?.('#footer,#global_header,.login_bottom_row') && node.parentNode) {
- node.remove();
- }
- if (node.querySelectorAll) {
- node.querySelectorAll('#footer,#global_header,.login_bottom_row').forEach(el => {
- if (el && el.parentNode) el.remove();
- });
- }
- }
- }
- });
- observer.observe(document.documentElement, { childList: true, subtree: true });
- window.addEventListener('load', () => observer.disconnect(), { once: true });
- }
- // Initialization
- document.addEventListener('DOMContentLoaded', async () => {
- ui.init();
- auth.setupLogout();
- if (url.isLogin()) return auth.setupLoginCapture();
- if (url.isGames()) return games.waitForData();
- if (url.isFamilyPin()) {
- ui.notify('Family Pin Protected', 'Account is protected by family pin. Logging out...');
- setTimeout(() => typeof Logout === 'function' && Logout(), 1000);
- return;
- }
- if (!await auth.checkState()) return (location.href = CONFIG.urls.login);
- if (url.isProfile()) {
- const steamId = url.getSteamId();
- if (!steamId) return;
- const profiles = storage.get(CONFIG.storage.profiles);
- const idKey = await games.resolveID(steamId);
- const prevCount = profiles[idKey];
- if (prevCount !== undefined) {
- const currCount = await games.getCount(steamId);
- if (!currCount || currCount === prevCount) return;
- }
- const base = url.getBase();
- base && (location.href = base + CONFIG.urls.games);
- }
- });
- })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址