// ==UserScript== // @name Steam 社区市场增强 // @name:en Steam Community Market Enhancer // @namespace steam-market-enhancer // @version 2.2.1 // @description Steam 社区市场增强:挂单汇总与超价/低价检测、一键改价重挂、批量下架与自主定价、订单深度(买卖盘全量档位 + 税后到手 + 各变体最低价)、价格历史趋势、我的挂单市场排名、库存页价格标签与快速出售、交易报价价值摘要、市场交易记录分析与导出(CSV/TSV/JSON/Markdown)、我的池子行情盘、跨平台挂刀比价、补充包制作器利润计算、价格提醒。v2.0.0 全面适配 Steam 新版 React 市场页:统一走 ← 数据容器 // 页面内联脚本读它后 e.remove(),再赋值 // window.SSR = {loaderData, clientAssets, renderContext} // ⚠️ 旧标记 window.SSR.renderContext=JSON.parse("...) 已彻底消失。 // renderContext.queryData 仍是**嵌套 JSON 字符串**,需二次 parse。 /** 从 HTML 文本中解析 valve-ssr-data → {loaderData, renderContext, config, userConfig} */ function smeParseValveSsr(html) { if (!html || typeof html !== 'string') return null; const m = html.match(/]*id=["']valve-ssr-data["'][^>]*>([\s\S]*?)<\/script>/i); if (!m) return null; let data; try { data = JSON.parse(m[1]); } catch (e) { return null; } if (!data || typeof data !== 'object') return null; return { loaderData: data.loaderData || null, renderContext: data.renderContext || null, clientAssets: data.clientAssets || null, config: data.Config || null, userConfig: data.UserConfig || null, }; } /** 归一化 React Query dehydrated state → [{key, data}](queryData 是嵌套 JSON 字符串) */ function smeSsrQueries(ssr) { const out = []; if (!ssr) return out; let rc = ssr.renderContext; if (typeof rc === 'string') { try { rc = JSON.parse(rc); } catch (e) { rc = null; } } if (!rc || typeof rc !== 'object') return out; let qd = rc.queryData; if (typeof qd === 'string') { try { qd = JSON.parse(qd); } catch (e) { qd = null; } } if (!qd || !Array.isArray(qd.queries)) return out; for (const q of qd.queries) { if (!q || !Array.isArray(q.queryKey)) continue; out.push({ key: q.queryKey, data: q.state ? q.state.data : null }); } return out; } /** * 按 queryKey 前缀查找。 * @param {Array} queries smeSsrQueries 的结果 * @param {Array} prefix 需匹配的前缀片段,如 ['market','orderbook'] * @param {string} [tail] 需完全相等的**末位**元素。 * 市场类 queryKey 形如 ['market','pricehistory',730,'<变体名>'], * 变体名在**最后一位**(不是 prefix.length 位)——故按末位比对。 */ function smeSsrFindQuery(queries, prefix, tail) { for (const q of queries) { const k = q.key; if (k.length < prefix.length) continue; let ok = true; for (let i = 0; i < prefix.length; i++) { if (String(k[i]) !== String(prefix[i])) { ok = false; break; } } if (!ok) continue; if (tail !== undefined && String(k[k.length - 1]) !== String(tail)) continue; return q; } return null; } /** * 解析同款家族聚合页的变体清单。 * loaderData[3].buckets[] = {bucket_id: , classid} * @returns {Array<{name:string, classid:string}>} */ function smeSsrVariants(ssr) { const out = []; if (!ssr) return out; const ld = ssr.loaderData; const entries = Array.isArray(ld) ? ld : (ld && typeof ld === 'object' ? Object.values(ld) : []); for (const entry of entries) { let d = entry; if (typeof d === 'string') { try { d = JSON.parse(d); } catch (e) { continue; } } if (!d || !Array.isArray(d.buckets)) continue; for (const b of d.buckets) { if (!b) continue; const name = b.bucket_id || b.market_hash_name || b.name; if (!name) continue; if (out.some(v => v.name === name)) continue; out.push({ name: String(name), classid: b.classid != null ? String(b.classid) : '' }); } } return out; } /** 焦点变体:initialSelectedBucketID → initialFallbackBucketID → buckets[0] */ function smeSsrFocusVariant(ssr) { if (!ssr) return null; const ld = ssr.loaderData; const entries = Array.isArray(ld) ? ld : (ld && typeof ld === 'object' ? Object.values(ld) : []); for (const entry of entries) { let d = entry; if (typeof d === 'string') { try { d = JSON.parse(d); } catch (e) { continue; } } if (!d || !Array.isArray(d.buckets) || !d.buckets.length) continue; const byId = (id) => d.buckets.find(b => b && String(b.bucket_id) === String(id)); const b = (d.initialSelectedBucketID != null && byId(d.initialSelectedBucketID)) || (d.initialFallbackBucketID != null && byId(d.initialFallbackBucketID)) || d.buckets[0]; if (b && b.bucket_id) return String(b.bucket_id); } return null; } /** * 从 SSR 取订单簿(**变体感知**)。 * * ★ v2.0.0 修复的静默 bug:同款家族聚合页一次 SSR 里混着多个变体的数据 —— * market/orderbook 对应 initialFallbackBucketID(实测 Minimal Wear), * market_item_search 的 listings 对应 buckets[0](实测 Battle-Scarred), * 而 URL 可能 302 自第三个变体(实测 Field-Tested)。 * 旧实现盲取第一个 rgCompactSellOrders → 会把**别的变体的盘**当成当前变体展示, * 导致超价/低价检测与定价建议全部失真。 * 现在:返回盘所属变体名 + 是否与目标一致,由调用方决定提示还是切变体。 * * @param {object} ssr smeParseValveSsr 的结果 * @param {string} [targetName] 目标变体 market_hash_name(用于比对) * @returns {object|null} {sellLevels, buyLevels, highestBuyCents, lowestSellCents, currency, variantName, matched} */ function smeSsrOrderBook(ssr, targetName) { const queries = smeSsrQueries(ssr); const q = smeSsrFindQuery(queries, ['market', 'orderbook']); if (!q || !q.data) return null; const raw = q.data; const sellLevels = smeCompactToLevels(raw.rgCompactSellOrders); const buyLevels = smeCompactToLevels(raw.rgCompactBuyOrders); if (!sellLevels.length && !buyLevels.length && raw.amtMaxBuyOrder == null && raw.amtMinSellOrder == null) { return null; } // queryKey 第 4 位就是该盘所属的变体名 const variantName = q.key.length > 3 ? String(q.key[3]) : ''; const matched = !targetName || !variantName || variantName === String(targetName); return { sellLevels: sellLevels, buyLevels: buyLevels, highestBuyCents: raw.amtMaxBuyOrder != null ? parseInt(raw.amtMaxBuyOrder, 10) : null, lowestSellCents: raw.amtMinSellOrder != null ? parseInt(raw.amtMinSellOrder, 10) : null, currency: raw.eCurrency != null ? parseInt(raw.eCurrency, 10) : null, buyOrderCount: raw.cBuyOrders != null ? parseInt(raw.cBuyOrders, 10) : null, sellOrderCount: raw.cSellOrders != null ? parseInt(raw.cSellOrders, 10) : null, variantName: variantName, matched: matched, }; } /** 压缩数组 [price, qty, price, qty, ...] → [{cents, qty}] */ function smeCompactToLevels(arr) { const out = []; if (!Array.isArray(arr)) return out; for (let i = 0; i + 1 < arr.length; i += 2) { const cents = parseInt(arr[i], 10); const qty = parseInt(arr[i + 1], 10); if (isFinite(cents) && cents > 0 && qty > 0) out.push({ cents: cents, qty: qty }); } return out; } /** * 从 SSR 取价格历史(新版格式:{time, price_median, purchases})。 * 比旧版 API 的 [[date, price, vol]] 多一个**中位价**口径 —— 中位价比裸成交均价 * 更能代表「市场真实价」,抗单笔异常成交。 * @returns {{prices:Array<{t:number, medianCents:number, vol:number}>, ecurrency:number}|null} */ function smeSsrPriceHistory(ssr, name) { const queries = smeSsrQueries(ssr); const q = smeSsrFindQuery(queries, ['market', 'pricehistory'], name !== undefined ? String(name) : undefined); if (!q || !q.data) return null; const rows = q.data.prices; if (!Array.isArray(rows) || !rows.length) return null; const prices = []; for (const r of rows) { if (!r) continue; const t = typeof r.time === 'number' ? r.time : parseInt(r.time, 10); if (!isFinite(t)) continue; const median = typeof r.price_median === 'number' ? r.price_median : parseFloat(r.price_median); if (!isFinite(median)) continue; const vol = parseInt(r.purchases, 10) || 0; // price_median 是**货币单位**(如 194.03)。 // p = 单位金额(与旧版 pricehistory API 的 prices[1] 同口径,消费方无需改) // medianCents = 同一价格的分值(明确口径,避免各处再乘/除 100 出错) prices.push({ t: t * 1000, p: median, medianCents: Math.round(median * 100), vol: vol, v: vol }); } if (!prices.length) return null; return { prices: prices, ecurrency: q.data.ecurrency != null ? parseInt(q.data.ecurrency, 10) : null, }; } /** * 从 SSR 取挂单列表(market_item_search)。 * @returns {{start:number, total:number, listings:Array<{id,cents,bMine,classid,strSubtotal,eCurrency,publisherFeePct}}}|null} */ function smeSsrListings(ssr) { const queries = smeSsrQueries(ssr); const q = smeSsrFindQuery(queries, ['market_item_search']); if (!q || !q.data || !Array.isArray(q.data.pages) || !q.data.pages.length) return null; const page = q.data.pages[q.data.pages.length - 1]; const listings = []; for (const l of (page.listings || [])) { if (!l) continue; listings.push({ id: String(l.listingid || ''), cents: parseInt(l.unPrice, 10) || 0, feeCents: parseInt(l.unFee, 10) || 0, strSubtotal: l.strSubtotal || '', bMine: l.bMine === true, eCurrency: l.eCurrency != null ? parseInt(l.eCurrency, 10) : null, publisherFeePct: typeof l.publisherFeePct === 'number' ? l.publisherFeePct : null, classid: l.description && l.description.classid != null ? String(l.description.classid) : '', }); } return { start: page.start || 0, total: page.total_count || 0, more: page.more === true, listings: listings, }; } /** * 从 SSR 取钱包上下文(新版页不再有 g_rgWalletInfo / v_currencyformat)。 * @returns {{currency:number|null, country:string, language:string, publisherFeePct:number|null}} */ function smeSsrWallet(ssr) { const out = { currency: null, country: '', language: '', publisherFeePct: null }; if (!ssr) return out; if (ssr.config) { out.country = ssr.config.COUNTRY || ''; out.language = ssr.config.LANGUAGE || ''; } const queries = smeSsrQueries(ssr); // 币种:优先 orderbook.eCurrency,其次 market/description.currency const ob = smeSsrFindQuery(queries, ['market', 'orderbook']); if (ob && ob.data && ob.data.eCurrency != null) out.currency = parseInt(ob.data.eCurrency, 10); if (out.currency == null) { const desc = queries.find(q => q.key[0] === 'market' && q.key[1] === 'description' && q.data); if (desc && desc.data && desc.data.currency != null) { // description.currency 是布尔(false = 非特殊货币),不能用;改用 orderbook // 拿不到时退化为从 listings 的 eCurrency 取 } } if (out.currency == null) { const ls = smeSsrFindQuery(queries, ['market_item_search']); if (ls && ls.data && Array.isArray(ls.data.pages) && ls.data.pages.length) { const l0 = (ls.data.pages[ls.data.pages.length - 1].listings || [])[0]; if (l0 && l0.eCurrency != null) out.currency = parseInt(l0.eCurrency, 10); } } // 发行商费率(按游戏):listings[].publisherFeePct(实测 0.10) const ls2 = smeSsrFindQuery(queries, ['market_item_search']); if (ls2 && ls2.data && Array.isArray(ls2.data.pages) && ls2.data.pages.length) { const l0 = (ls2.data.pages[ls2.data.pages.length - 1].listings || [])[0]; if (l0 && typeof l0.publisherFeePct === 'number') out.publisherFeePct = l0.publisherFeePct; } return out; } // ------------------------------------------------------------------ // ③ 纯计算:定价策略 / 价格提醒 / 库存浮盈 / 交易冷却 // ------------------------------------------------------------------ /** * 窗口中位价(v2.0.0):给「近 N 日中位价」定价口径用。 * 兼容两种历史数据结构: * · 新版 SSR:{t, medianCents, vol} —— 每日中位价(已是分) * · 旧版 API:{t, p, v} —— 逐笔成交价(p 是**货币单位**) * @param {object} priceHistory {prices:[...]} * @param {number} days 窗口天数 * @returns {number} 分;无数据返回 0 */ function calcWindowMedianCents(priceHistory, days) { if (!priceHistory || !Array.isArray(priceHistory.prices) || !priceHistory.prices.length) return 0; const win = (days && days > 0) ? days : 30; const cutoff = Date.now() - win * 86400000; const vals = []; for (const p of priceHistory.prices) { if (!p || !(p.t >= 0)) continue; if (p.t < cutoff) continue; let cents = 0; if (p.medianCents != null) cents = p.medianCents; else if (p.p != null) cents = Math.round(p.p * 100); if (cents > 0) vals.push(cents); } if (!vals.length) return 0; vals.sort((a, b) => a - b); const mid = Math.floor(vals.length / 2); return vals.length % 2 ? vals[mid] : Math.round((vals[mid - 1] + vals[mid]) / 2); } /** * 批量定价策略(v2.0.0 新增)。 * 社区「一键挂卡」标准流程的三种口径: * undercut 最低卖价 - N 分(抢单) * median 近 N 日中位价(按市场真实价) * ratio 跨平台目标价(挂刀比价) * @param {object} opt {lowestSellCents, medianCents, targetCents, offsetCents} * @returns {{cents:number, source:string}|null} */ function smeStrategyPrice(opt) { if (!opt) return null; const mode = opt.mode || 'undercut'; const offset = opt.offsetCents || 0; if (mode === 'median') { if (!opt.medianCents) return null; return { cents: Math.max(0, opt.medianCents + offset), source: 'median' }; } if (mode === 'ratio') { if (!opt.targetCents) return null; return { cents: Math.max(0, opt.targetCents + offset), source: 'ratio' }; } if (!opt.lowestSellCents) return null; return { cents: Math.max(0, opt.lowestSellCents - 1 + offset), source: 'undercut' }; } /** * 挂刀目标价(v2.0.0 新增,纯计算): * 平台最低价(CNY 分)× 平台费系数 → 平台到手 → 换算成本地货币的**等效 Steam 售价**。 * 语义:把挂单价压到这个数,在 Steam 卖 ≈ 在那个平台卖。 * @param {number} platformLowCnyCents 平台最低卖价(CNY 分) * @param {number} feeFactor 平台费系数(0~1) * @param {number} cnyToLocal CNY→本地货币汇率(1 = 同币种) * @returns {number} 本地货币分;无法换算返回 0 */ function smeRatioTargetCents(platformLowCnyCents, feeFactor, cnyToLocal) { if (!(platformLowCnyCents > 0)) return 0; const fee = (feeFactor > 0 && feeFactor <= 1) ? feeFactor : 0.85; const netCny = Math.round(platformLowCnyCents * fee); if (!(netCny > 0)) return 0; if (!(cnyToLocal > 0)) return 0; return Math.round(netCny * cnyToLocal); } /** * 价格提醒判定(v2.0.0 新增)。 * 阈值语义:现价 ≤ 基准价 × (1 - dropPct) 即触发。 * 基准默认取 30 日中位价(抗单笔异常)。 * @param {object} o {currentCents, baseCents, dropPct, lastNotifiedCents} * @returns {{hit:boolean, drop:number, reason:string}} */ function smeAlertCheck(o) { if (!o || !o.currentCents || !o.baseCents) return { hit: false, drop: 0, reason: 'no-data' }; const dropPct = o.dropPct != null ? o.dropPct : 0.1; const threshold = o.baseCents * (1 - dropPct); const drop = (o.baseCents - o.currentCents) / o.baseCents; if (o.currentCents > threshold) return { hit: false, drop: drop, reason: 'above' }; // 幂等:同一价位只通知一次(避免每轮轮询重复推送) if (o.lastNotifiedCents != null && o.currentCents >= o.lastNotifiedCents) { return { hit: false, drop: drop, reason: 'already' }; } return { hit: true, drop: drop, reason: 'triggered' }; } /** * 成本基准引擎(FIFO,v2.0.0 新增)。 * * 口径纪律(错一条整张表就歪): * · **成本基准用「卖家到手价」**,不是买家支付价。 * 成交记录里的 priceCents 是买家支付价(含手续费);拿它当成本, * 会让「买入即亏手续费」,成本虚高、浮亏虚高。netOf 负责换算。 * · **卖出按 FIFO 消耗买入批次** → 已实现盈亏才有正确配比。 * · **卖出找不到对应买入记录**(买入发生在历史窗口之外)→ * 单独记 unknownCost 计数,**不猜成本**,UI 如实标注「成本未知」。 * * @param {Array} rows 成交记录 [{name, action:'bought'|'sold', priceCents, tradeTs, game}] * @param {object} [opts] {netOf: (priceCents)=>到手分} * @returns {{holdings:Array, realized:Map, unmatchedSales:number}} */ function smeCostBasis(rows, opts) { const o = opts || {}; const netOf = typeof o.netOf === 'function' ? o.netOf : (c) => Math.round(c / 1.15); // 默认:Steam 5% + 发行商 10% const list = (Array.isArray(rows) ? rows.slice() : []); // 按时间升序;无时间的排到最后(不参与 FIFO 顺序推断) list.sort((a, b) => (a.tradeTs || a.listedTs || 0) - (b.tradeTs || b.listedTs || 0)); const lots = new Map(); // name → [{qty, costCents, ts}] const realized = new Map(); // name → {qty, proceedsCents, costCents, unknownCost} let unmatchedSales = 0; for (const r of list) { const name = String(r.name || ''); if (!name) continue; const ts = r.tradeTs || r.listedTs || 0; if (r.action === 'bought') { if (!lots.has(name)) lots.set(name, []); lots.get(name).push({ qty: 1, costCents: netOf(r.priceCents || 0), ts: ts }); } else if (r.action === 'sold') { const arr = lots.get(name); if (!arr || !arr.length) { // 买入发生在索引窗口之外 → 成本未知,不猜 if (!realized.has(name)) realized.set(name, { qty: 0, proceedsCents: 0, costCents: 0, unknownCost: 0 }); const rec = realized.get(name); rec.qty++; rec.proceedsCents += netOf(r.priceCents || 0); rec.unknownCost++; unmatchedSales++; continue; } const lot = arr.shift(); if (!realized.has(name)) realized.set(name, { qty: 0, proceedsCents: 0, costCents: 0, unknownCost: 0 }); const rec = realized.get(name); rec.qty++; rec.proceedsCents += netOf(r.priceCents || 0); rec.costCents += lot.costCents; } } const holdings = []; for (const [name, arr] of lots) { let qty = 0, totalCost = 0, oldestTs = 0, newestTs = 0; for (const l of arr) { qty += l.qty; totalCost += l.costCents; if (l.ts) { if (!oldestTs || l.ts < oldestTs) oldestTs = l.ts; if (l.ts > newestTs) newestTs = l.ts; } } if (!qty) continue; holdings.push({ name: name, qty: qty, costCents: qty ? Math.round(totalCost / qty) : 0, // 单位成本(分),与 smePnlCalc 同口径 costTotalCents: totalCost, oldestTs: oldestTs, newestTs: newestTs, lots: arr.map(l => ({ qty: l.qty, costCents: l.costCents, ts: l.ts })), }); } holdings.sort((a, b) => (b.costTotalCents - a.costTotalCents)); return { holdings: holdings, realized: realized, unmatchedSales: unmatchedSales }; } /** * 交易冷却清单(v2.0.0 新增)。 * * 规则依据: * · 2024-10-02 起:市场/军械库/交易报价/游戏内商店获得的物品,7 天内不可再交易/上架 * → 冷却天数取自 Steam 自己给的 descriptions[].market_marketable_restriction * (Valve 游戏为 7)与 market_tradable_restriction。 * · 2025-07-16 起:CS2 交易所得物品有 7 天 Trade Protection(可被撤销)。 * * 获得时间来自成交记录(市场买入)。**交易/赠送所得的物品没有买入记录 → * 起始时间未知**,此时只标「锁定中·来源未知」,不编造解锁日期。 * * @param {Array} holdings smeCostBasis().holdings * @param {Object} restrictionDays {name: {marketableDays, tradableDays}} * @param {number} now 当前时间戳 * @returns {Array} 冷却中的持仓 [{name, qty, days, daysLeft, untilTs, earliestUntilTs, source}] */ function smeCooldownList(holdings, restrictionDays, now) { const out = []; const list = Array.isArray(holdings) ? holdings : []; for (const hold of list) { const rule = restrictionDays && restrictionDays[hold.name]; if (!rule) continue; const days = parseInt(rule.marketableDays != null ? rule.marketableDays : rule.tradableDays, 10) || 0; if (days <= 0) continue; if (!hold.newestTs) { // 获得时间未知:无法算解锁日 out.push({ name: hold.name, qty: hold.qty, days: days, daysLeft: null, untilTs: 0, earliestUntilTs: 0, source: 'unknown', }); continue; } const untilTs = hold.newestTs + days * 86400000; const earliestUntilTs = (hold.oldestTs || hold.newestTs) + days * 86400000; const daysLeft = now >= untilTs ? 0 : Math.ceil((untilTs - now) / 86400000); if (daysLeft <= 0) continue; out.push({ name: hold.name, qty: hold.qty, days: days, daysLeft: daysLeft, untilTs: untilTs, earliestUntilTs: earliestUntilTs, source: 'market-buy', }); } out.sort((a, b) => (b.daysLeft || 999) - (a.daysLeft || 999)); return out; } /** * 库存浮盈浮亏(v2.0.0 新增)。 * 成本基准 = 成交记录里的**买入均价**(按 market_hash_name 聚合)。 * @param {Array} holdings [{name, qty, costCents}] * @param {Object} priceMap {name: currentCents} * @returns {{rows:Array, totalCost:number, totalValue:number, totalPnl:number, priced:number, total:number}} */ function smePnlCalc(holdings, priceMap) { const rows = []; let totalCost = 0, totalValue = 0, priced = 0; const list = Array.isArray(holdings) ? holdings : []; for (const h of list) { if (!h || !h.name) continue; const qty = h.qty || 0; const costCents = h.costCents || 0; const cur = priceMap && priceMap[h.name] != null ? priceMap[h.name] : null; const cost = costCents * qty; const value = cur != null ? cur * qty : null; totalCost += cost; if (value != null) { totalValue += value; priced++; } rows.push({ name: h.name, qty: qty, costCents: costCents, currentCents: cur, pnlCents: value != null ? value - cost : null, pnlPct: (value != null && cost > 0) ? (value - cost) / cost : null, }); } return { rows: rows, totalCost: totalCost, totalValue: totalValue, totalPnl: totalValue - totalCost, priced: priced, total: list.length, }; } /** * 交易冷却 / 锁定判定(v2.0.0 新增)。 * 2024-10-02 起:市场/军械库/交易获得的物品 7 天内不可再上架。 * 2025-07-16 起:CS2 交易所得物品 7 天 Trade Protection(可被撤销,撤销者禁交易+市场 30 天)。 * @param {number} acquiredAt 获得时间戳(毫秒);未知传 0 * @param {number} now 当前时间戳 * @param {number} [cooldownDays] 冷却天数,默认 7 * @returns {{locked:boolean, until:number, daysLeft:number}} */ function smeCooldownCalc(acquiredAt, now, cooldownDays) { const days = cooldownDays != null ? cooldownDays : 7; if (!acquiredAt || !now) return { locked: false, until: 0, daysLeft: 0 }; const until = acquiredAt + days * 86400000; if (now >= until) return { locked: false, until: until, daysLeft: 0 }; return { locked: true, until: until, daysLeft: Math.ceil((until - now) / 86400000), }; } // ==================== v2.0.0 纯计算层结束 ==================== // ==================== 启动(等待 URL 稳定) ==================== let stableSince = 0; let lastCheck = location.href; let domReady = false; function isOnMarket() { return /^\/market(?:\/|$)/.test(location.pathname) || /^\/(?:id|profiles)\/[^/]+\/inventory/.test(location.pathname) || /^\/tradeoffer/.test(location.pathname) || /^\/tradingcards\/boostercreator/.test(location.pathname); } function tryStart() { if (!domReady) return; const now = Date.now(); const curURL = location.href; if (curURL !== lastCheck) { lastCheck = curURL; stableSince = now; return; } if (now - stableSince < 800) return; if (!isOnMarket()) return; clearInterval(stableTimer); init(); } let stableTimer; function onDOMReady() { domReady = true; stableSince = Date.now(); lastCheck = location.href; stableTimer = setInterval(tryStart, 250); setTimeout(() => { if (domReady && isOnMarket() && !initialized) { clearInterval(stableTimer); init().catch(e => W('初始化失败:', e && e.message)); } }, 2000); } // v1.5.0:显示更多价格——必须在 document-start、页面 react-dom 初始化前注入假 DevTools hook, // 因此这里随脚本执行同步调用(不能放进 init:init 要等 DOM ready,届时 react-dom 早已初始化) if (settings.get('showMorePrices') && /^\/market\/listings\/\d+\/.+/.test(location.pathname)) { installShowMoreHook(); } if (document.readyState === 'interactive' || document.readyState === 'complete') { onDOMReady(); } else { document.addEventListener('DOMContentLoaded', onDOMReady); } })();