// ==UserScript== // @name SGLV ITAD Library // @namespace sglv-lib // @version 1.0.3 // @description SGLV ITAD (IsThereAnyDeal) API 封装库,统一封装 ITAD 价格相关接口。 // 端点覆盖:lookup/id (AppID→GameID) / games/overview (当前最优+史低+Bundle) / games/prices (多商店当前价) / // games/history (价格历史) / games/historylow (史低) / games/storelow (单商店最低) / service/shops (商店列表) / testConnection (密钥验证)。 // 设计目标:UI 无关、零业务依赖、可被多个插件复用、与 SGLVCore 网络层解耦。 // 暴露 SGLVITAD:init / setApiKey / hasApiKey / lookupItadId / fetchPriceOverview / fetchPrices / // fetchMultiRegionPrices / fetchPriceHistory / fetchHistoryLow / fetchStoreLow / getShops / testConnection / clearCache。 // 参考项目:https://github.com/sys1em/Steam_Buff (shared/config.js) / https://docs.isthereanydeal.com/ // @author SGLV // @noframes // ==/UserScript== /* * SGLV ITAD Library v1.0.0 * * 模块切分: * 1) 网络层 _httpXhr / _getJson / _postJson (gmFetchJson 优先, 降级 GM_xmlhttpRequest / fetch) * 2) 缓存层 _cacheGet / _cacheSet (GM_setValue 持久化, TTL 过期自动清理) * 3) ID 映射层 lookupItadId / lookupItadIds (Steam AppID → ITAD GameID, 批量/单个) * 4) 价格概览层 fetchPriceOverview (overview/v2: 当前最优 + 历史最低 + 活跃 Bundle) * 5) 多商店价格层 fetchPrices / fetchMultiRegionPrices (prices/v3: 多商店/多区域当前价) * 6) 历史价格层 fetchPriceHistory (history/v2: 价格变更日志 + 折扣去重) * 7) 史低/单店低 fetchHistoryLow / fetchStoreLow * 8) 工具层 getShops / testConnection * 9) 归一化层 normalizeOverview / normalizePrices / normalizeHistory / normalizePrice * * 设计契约: * - 库不依赖 window.SGLVCore 之外的其他 SGLV 库 * - API Key 由宿主通过 init()/setApiKey() 注入,库自身不读取 GM 存储 * - 不引入任何 UI 渲染逻辑,仅返回结构化数据 * - 所有响应经过归一化处理,保证数据结构一致性 * - 缓存键前缀 sglv_itad_,可通过 init({ cachePrefix }) 自定义 */ (function (root) { 'use strict'; // 防止重复挂载 if (root.SGLVITAD && root.SGLVITAD.version === '1.0.3') return; // ==================== 常量 ==================== const VERSION = '1.0.3'; const ITAD_BASE_URL = 'https://api.isthereanydeal.com'; const ITAD_SHOP_STEAM = 61; // Steam 商店 ID // 缓存 TTL (可被 init() 覆盖) const DEFAULT_TTL = { lookup: 24 * 3600 * 1000, // AppID→GameID 映射: 24h (映射关系不变) overview: 2 * 3600 * 1000, // 价格概览: 2h (价格变化较频繁) prices: 2 * 3600 * 1000, // 多商店/多区域价格: 2h history: 6 * 3600 * 1000, // 价格历史: 6h historyLow: 6 * 3600 * 1000, // 史低: 6h storeLow: 6 * 3600 * 1000, // 单商店最低: 6h shops: 7 * 24 * 3600 * 1000, // 商店列表: 7天 (极少变化) info: 7 * 24 * 3600 * 1000, // 游戏信息: 7天 }; const DEFAULT_TIMEOUT = 15000; const HISTORY_TIMEOUT = 20000; const MAX_BATCH = 200; // ITAD API 单次最多 200 个 GameID // ==================== 状态 ==================== let _apiKey = ''; let _ttl = Object.assign({}, DEFAULT_TTL); let _cachePrefix = 'sglv_itad_'; // ==================== 网络层 ==================== // 降级路径: 无 SGLVCore 时用本地 GM_xmlhttpRequest + fetch // 与 SGLVBadge 网络层设计一致 // 注意: SGLVCore.gmFetchText 硬编码 method:'GET',不支持 POST; // JSON 请求必须走 gmFetchJson (正确传递 method/headers/data) 或本地 _httpXhr function _httpXhr(opts) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest === 'function') { GM_xmlhttpRequest({ method: opts.method || 'GET', url: opts.url, headers: opts.headers || {}, data: opts.data, timeout: opts.timeout || DEFAULT_TIMEOUT, anonymous: false, onload(r) { if (r.status >= 200 && r.status < 300) resolve(r); else reject(new Error('HTTP ' + r.status)); }, onerror: () => reject(new Error('network error')), ontimeout: () => reject(new Error('timeout')), }); return; } // fetch 降级 (非油猴环境, 如测试) const ctl = (typeof AbortController === 'function') ? new AbortController() : null; const timer = ctl ? setTimeout(() => ctl.abort(), opts.timeout || DEFAULT_TIMEOUT) : null; fetch(opts.url, { method: opts.method || 'GET', headers: opts.headers || {}, body: opts.data, signal: ctl ? ctl.signal : undefined, }) .then(r => { if (timer) clearTimeout(timer); if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); }) .then(text => resolve({ status: 200, responseText: text })) .catch(err => { if (timer) clearTimeout(timer); reject(err); }); }); } async function _getJson(url, opts) { // 优先使用 SGLVCore.gmFetchJson — 正确传递 method/headers/data, 支持 POST if (root.SGLVCore && root.SGLVCore.gmFetchJson) { return root.SGLVCore.gmFetchJson(url, opts || {}); } // 降级: 本地 GM_xmlhttpRequest / fetch const r = await _httpXhr(Object.assign({ url }, opts || {})); try { return JSON.parse(r.responseText); } catch (e) { throw new Error('invalid JSON'); } } async function _postJson(url, body, opts) { return _getJson(url, Object.assign({ method: 'POST', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(body), }, opts || {})); } // ==================== 缓存层 ==================== // 基于 GM_setValue 的 TTL 缓存, 与主脚本 cacheGet/cacheSet 模式一致 function _cacheGet(key) { if (typeof GM_getValue !== 'function') return null; try { const raw = GM_getValue(_cachePrefix + key, ''); if (!raw) return null; const obj = typeof raw === 'string' ? JSON.parse(raw) : raw; if (obj.expires && Date.now() > obj.expires) { if (typeof GM_setValue === 'function') GM_setValue(_cachePrefix + key, ''); return null; } return obj.data; } catch { return null; } } function _cacheSet(key, data, ttl) { if (typeof GM_setValue !== 'function') return; try { GM_setValue(_cachePrefix + key, JSON.stringify({ data, expires: Date.now() + ttl })); } catch { /* ignore quota errors */ } } // ==================== URL 构建 ==================== // 使用字符串拼接 (遵循项目约定: 避免 new URL() 相对路径问题) function _buildUrl(path, params) { let url = ITAD_BASE_URL + path + '?key=' + encodeURIComponent(_apiKey); if (params) { for (const k of Object.keys(params)) { if (params[k] != null && params[k] !== '') { url += '&' + encodeURIComponent(k) + '=' + encodeURIComponent(String(params[k])); } } } return url; } // ==================== ID 映射层 ==================== /** * Steam AppID → ITAD GameID (单个) * POST /lookup/id/shop/{shopId}/v1 * * @param {number|string} appId - Steam App ID * @param {number} shopId - 商店 ID (默认 61 = Steam) * @returns {Promise} ITAD Game UUID, 未找到返回 null */ async function lookupItadId(appId, shopId) { shopId = shopId || ITAD_SHOP_STEAM; const formattedId = 'app/' + appId; const cacheKey = 'lookup_' + shopId + '_' + appId; const cached = _cacheGet(cacheKey); if (cached !== null) return cached; try { const data = await _postJson( ITAD_BASE_URL + '/lookup/id/shop/' + shopId + '/v1?key=' + encodeURIComponent(_apiKey), [formattedId], { timeout: DEFAULT_TIMEOUT } ); const itadId = (data && data[formattedId]) ? data[formattedId] : null; if (itadId) _cacheSet(cacheKey, itadId, _ttl.lookup); return itadId; } catch (e) { console.warn('[SGLVITAD] lookupItadId failed:', e.message); return null; } } /** * Steam AppID → ITAD GameID (批量) * POST /lookup/id/shop/{shopId}/v1 * * @param {Array} appIds - Steam App IDs (最多 200) * @param {number} shopId - 商店 ID * @returns {Promise} { "app/123": "uuid", "app/456": null, ... } */ async function lookupItadIds(appIds, shopId) { shopId = shopId || ITAD_SHOP_STEAM; const batch = appIds.slice(0, MAX_BATCH); const formatted = batch.map(function (id) { return 'app/' + id; }); try { const data = await _postJson( ITAD_BASE_URL + '/lookup/id/shop/' + shopId + '/v1?key=' + encodeURIComponent(_apiKey), formatted, { timeout: DEFAULT_TIMEOUT } ); return data || {}; } catch (e) { console.warn('[SGLVITAD] lookupItadIds failed:', e.message); return {}; } } // ==================== 游戏信息 ==================== /** * 获取游戏详细信息 (发行日期、标题等) * GET /games/info/v2 * * @param {string} itadId - ITAD Game UUID * @returns {Promise} { title, slug, releaseDate, ... } */ async function fetchGameInfo(itadId) { const cacheKey = 'info_' + itadId; const cached = _cacheGet(cacheKey); if (cached) return cached; try { const data = await _getJson( _buildUrl('/games/info/v2', { id: itadId }), { timeout: DEFAULT_TIMEOUT } ); _cacheSet(cacheKey, data, _ttl.info); return data; } catch (e) { console.warn('[SGLVITAD] fetchGameInfo failed:', e.message); return null; } } // ==================== 价格概览层 (核心增强端点) ==================== /** * 价格概览: 当前最优价 + 历史最低 + 活跃 Bundle (一次调用全覆盖) * POST /games/overview/v2 * * @param {string|Array} itadIds - ITAD Game UUID(s), 最多 200 * @param {Object} opts - { country, shops } * - country: ISO 3166-1 alpha-2, 默认 'US' * - shops: 商店 ID 数组, 如 [61] 仅 Steam; 不传则全部商店 * @returns {Promise} 归一化后的 { prices: [...], bundles: [...] } */ async function fetchPriceOverview(itadIds, opts) { opts = opts || {}; const country = opts.country || 'US'; const ids = Array.isArray(itadIds) ? itadIds.slice(0, MAX_BATCH) : [itadIds]; const cacheKey = 'overview_' + country + '_' + ids.slice().sort().join(','); const cached = _cacheGet(cacheKey); if (cached) return cached; // v1.0.2: 仅发送 API 文档确认的参数, 移除 vouchers (非有效参数导致 400) const params = { country: country }; if (opts.shops && opts.shops.length) params.shops = opts.shops.join(','); try { const data = await _postJson(_buildUrl('/games/overview/v2', params), ids, { timeout: DEFAULT_TIMEOUT }); const result = normalizeOverview(data); _cacheSet(cacheKey, result, _ttl.overview); return result; } catch (e) { // v1.0.2: 包含 country 便于定位无效区域代码 console.warn('[SGLVITAD] fetchPriceOverview failed (country=' + country + '):', e.message); return null; } } // ==================== 多商店/多区域价格层 ==================== /** * 多商店当前价格 (含历史最低) * POST /games/prices/v3 * * @param {string|Array} itadIds - ITAD Game UUID(s) * @param {Object} opts - { country, deals, capacity, shops } * - country: ISO 3166-1 alpha-2, 默认 'US' * - deals: 仅返回有折扣的价格, 默认 false (仅 true 时发送) * - capacity: 每个游戏返回多少条价格 (0=不限, 默认) * - shops: 商店 ID 数组, 如 [61] 仅 Steam * @returns {Promise} 归一化后的价格数组 */ async function fetchPrices(itadIds, opts) { opts = opts || {}; const country = opts.country || 'US'; const deals = opts.deals || false; const capacity = opts.capacity || 0; const ids = Array.isArray(itadIds) ? itadIds.slice(0, MAX_BATCH) : [itadIds]; // v1.0.2: 仅发送 API 文档确认的参数, 移除 vouchers/deals=false (导致 400) const params = { country: country }; if (deals) params.deals = true; // 仅 deals=true 时发送 if (capacity > 0) params.capacity = capacity; // 仅 capacity>0 时发送 if (opts.shops && opts.shops.length) params.shops = opts.shops.join(','); try { const data = await _postJson(_buildUrl('/games/prices/v3', params), ids, { timeout: DEFAULT_TIMEOUT }); return normalizePrices(data); } catch (e) { // v1.0.2: 包含 country 便于定位无效区域代码 (如 EU 非有效 ISO 3166-1 alpha-2) console.warn('[SGLVITAD] fetchPrices failed (country=' + country + '):', e.message); return null; } } /** * 多区域 Steam 价格: 对多个 country 并发调用 prices/v3 (shops=[61] 仅 Steam) * 内部使用 mapLimit 模式限流并发, 与主脚本 fetchMultiRegionPrices 设计一致 * * @param {string} itadId - ITAD Game UUID * @param {Array} countries - ISO 3166-1 alpha-2 国家代码数组 * @param {Object} opts - { concurrency, shops } * @returns {Promise} { CN: { deals: [...] }, US: { deals: [...] }, ... } */ async function fetchMultiRegionPrices(itadId, countries, opts) { opts = opts || {}; const concurrency = opts.concurrency || 3; const shops = opts.shops || [ITAD_SHOP_STEAM]; const results = {}; // 简单 mapLimit 实现 (与主脚本 mapLimit 模式一致) const queue = (countries || ['US', 'CN', 'TR', 'AR', 'RU', 'IN', 'BR', 'UA', 'KZ']).slice(); let active = 0; let idx = 0; return new Promise(function (resolve) { function runNext() { while (active < concurrency && idx < queue.length) { const country = queue[idx++]; active++; fetchPrices([itadId], { country: country, shops: shops }) .then(function (data) { results[country] = (data && data[0]) ? data[0] : null; }) .catch(function () { results[country] = null; }) .finally(function () { active--; if (idx < queue.length) runNext(); else if (active === 0) resolve(results); }); } if (active === 0 && idx >= queue.length) resolve(results); } runNext(); }); } // ==================== 历史价格层 ==================== /** * 价格历史 + 折扣去重 (seenCuts 逻辑) * GET /games/history/v2 * * 先尝试不带 since 参数 (默认近 3 个月), 无数据则尝试 5 年范围 * * @param {string} itadId - ITAD Game UUID * @param {Object} opts - { country, shops, since } * @returns {Promise} 归一化后的 { history, discounts, lowest } */ async function fetchPriceHistory(itadId, opts) { opts = opts || {}; const country = opts.country || 'CN'; const shops = opts.shops || [ITAD_SHOP_STEAM]; const cacheKey = 'history_' + country + '_' + itadId; const cached = _cacheGet(cacheKey); if (cached) return cached; const params = { id: itadId, country: country, shops: shops.join(',') }; if (opts.since) params.since = opts.since; try { // 先尝试不带 since 参数 let data = await _getJson(_buildUrl('/games/history/v2', params), { timeout: HISTORY_TIMEOUT }); if (!data || !data.length) { // 尝试带 since 参数获取更早数据 (5 年前) var sinceDate = new Date(Date.now() - 365 * 5 * 86400000).toISOString().replace(/\.\d+Z$/, '+00:00'); params.since = sinceDate; data = await _getJson(_buildUrl('/games/history/v2', params), { timeout: HISTORY_TIMEOUT }); } var result = normalizeHistory(data || []); _cacheSet(cacheKey, result, _ttl.history); return result; } catch (e) { console.warn('[SGLVITAD] fetchPriceHistory failed:', e.message); return { history: [], discounts: [], lowest: null }; } } // ==================== 史低 / 单商店最低 ==================== /** * 历史最低价 (批量) * POST /games/historylow/v1 * * @param {string|Array} itadIds - ITAD Game UUID(s) * @param {Object} opts - { country } * @returns {Promise} { "uuid": { amount, currency, ... }, ... } */ async function fetchHistoryLow(itadIds, opts) { opts = opts || {}; var country = opts.country || 'US'; var ids = Array.isArray(itadIds) ? itadIds.slice(0, MAX_BATCH) : [itadIds]; var cacheKey = 'historylow_' + country + '_' + ids.slice().sort().join(','); var cached = _cacheGet(cacheKey); if (cached) return cached; try { var data = await _postJson(_buildUrl('/games/historylow/v1', { country: country }), ids, { timeout: DEFAULT_TIMEOUT }); _cacheSet(cacheKey, data, _ttl.historyLow); return data; } catch (e) { console.warn('[SGLVITAD] fetchHistoryLow failed:', e.message); return null; } } /** * 单商店最低价 (批量) * POST /games/storelow/v2 * * @param {string|Array} itadIds - ITAD Game UUID(s) * @param {Object} opts - { country, shops } * @returns {Promise} */ async function fetchStoreLow(itadIds, opts) { opts = opts || {}; var country = opts.country || 'US'; var shops = opts.shops; var ids = Array.isArray(itadIds) ? itadIds.slice(0, MAX_BATCH) : [itadIds]; var params = { country: country }; if (shops && shops.length) params.shops = shops.join(','); try { var data = await _postJson(_buildUrl('/games/storelow/v2', params), ids, { timeout: DEFAULT_TIMEOUT }); return data; } catch (e) { console.warn('[SGLVITAD] fetchStoreLow failed:', e.message); return null; } } // ==================== 工具层 ==================== /** * 获取所有商店列表 * GET /service/shops/v1 * * @returns {Promise} [{ id, name, ... }, ...] */ async function getShops() { var cacheKey = 'shops'; var cached = _cacheGet(cacheKey); if (cached) return cached; try { var data = await _getJson(_buildUrl('/service/shops/v1'), { timeout: DEFAULT_TIMEOUT }); _cacheSet(cacheKey, data, _ttl.shops); return data; } catch (e) { console.warn('[SGLVITAD] getShops failed:', e.message); return null; } } /** * 测试 API Key 连接 (调用 /service/shops/v1 验证) * * @param {string} [key] - 待测试的 Key (不传则使用当前已设置的 Key) * @returns {Promise} { success: boolean, error?: string, shopCount?: number } */ async function testConnection(key) { var testKey = key || _apiKey; if (!testKey) return { success: false, error: 'No API key provided' }; try { var url = ITAD_BASE_URL + '/service/shops/v1?key=' + encodeURIComponent(testKey); var data = await _getJson(url, { timeout: 10000 }); return { success: true, shopCount: Array.isArray(data) ? data.length : 0 }; } catch (e) { // v1.0.3: e 可能是非 Error 拒绝值(GM onerror 原始对象无 .message),先归一化再 match var msg = (e && typeof e.message === 'string' && e.message) ? e.message : String((e && typeof e === 'object' && e.error) ? e.error : (e || 'unknown error')); var match = msg.match(/HTTP (\d+)/); if (match && (match[1] === '401' || match[1] === '403')) { return { success: false, error: 'Invalid API key (HTTP ' + match[1] + ')' }; } return { success: false, error: msg }; } } // ==================== 响应归一化层 ==================== function normalizePrice(p) { if (!p) return null; return { amount: p.amount || 0, amountInt: p.amountInt || 0, currency: p.currency || '', }; } /** * 归一化 overview/v2 响应 * 输入: { prices: [...], bundles: [...] } * 输出: { prices: [{ id, current, lowest, bundled, urls }], bundles: [{ id, title, url, tiers, expiry }] } */ function normalizeOverview(data) { if (!data || !data.prices) return { prices: [], bundles: [] }; var prices = (data.prices || []).map(function (item) { var current = item.current; var lowest = item.lowest; return { id: item.id || '', current: current ? { shop: { id: (current.shop || {}).id, name: (current.shop || {}).name || '' }, price: normalizePrice(current.price), regular: normalizePrice(current.regular), cut: current.cut || 0, timestamp: current.timestamp || '', url: current.url || '', drm: current.drm || [], platforms: current.platforms || [], } : null, lowest: lowest ? { shop: { id: (lowest.shop || {}).id, name: (lowest.shop || {}).name || '' }, price: normalizePrice(lowest.price), regular: normalizePrice(lowest.regular), cut: lowest.cut || 0, timestamp: lowest.timestamp || '', } : null, bundled: item.bundled || 0, urls: item.urls || {}, }; }); var bundles = (data.bundles || []).map(function (b) { return { id: b.id || '', title: b.title || '', url: b.url || '', subsidized: b.subsidized || false, tiers: (b.tiers || []).map(function (t) { return { games: (t.games || []).map(function (g) { return { id: g.id || '', title: g.title || '' }; }), price: normalizePrice(t.price), regular: normalizePrice(t.regular), }; }), expiry: b.expiry || null, timestamp: b.timestamp || '', }; }); return { prices: prices, bundles: bundles }; } /** * 归一化 prices/v3 响应 * 输入: [{ id, historyLow: { all, y1, m3 }, deals: [...] }] * 输出: 同结构, 但 price 对象经过 normalizePrice 处理 */ function normalizePrices(data) { if (!Array.isArray(data)) return []; return data.map(function (item) { return { id: item.id || '', historyLow: { all: normalizePrice((item.historyLow || {}).all), y1: normalizePrice((item.historyLow || {}).y1), m3: normalizePrice((item.historyLow || {}).m3), }, deals: (item.deals || []).map(function (deal) { return { shop: { id: (deal.shop || {}).id, name: (deal.shop || {}).name || '' }, price: normalizePrice(deal.price), regular: normalizePrice(deal.regular), cut: deal.cut || 0, voucher: deal.voucher || null, storeLow: normalizePrice(deal.storeLow), flag: deal.flag || null, drm: deal.drm || [], platforms: deal.platforms || [], timestamp: deal.timestamp || '', expiry: deal.expiry || null, url: deal.url || '', }; }), }; }); } /** * 归一化 history/v2 响应: 解析折扣记录 (seenCuts 去重) * 输入: [{ timestamp, shop, deal: { cut, price, regular } }] * 输出: { history: [{ price, regular, currency, cut, store, date }], discounts: [...], lowest: {...}|null } */ function normalizeHistory(data) { if (!Array.isArray(data)) return { history: [], discounts: [], lowest: null }; var seenCuts = {}; var discounts = []; var allHistory = []; for (var i = 0; i < data.length; i++) { var item = data[i]; var deal = item.deal || {}; var cut = deal.cut || 0; var timestamp = item.timestamp || ''; var dealPrice = deal.price || {}; var regularPrice = deal.regular || {}; var shop = item.shop || ''; var shopName = typeof shop === 'string' ? shop : ((shop && shop.name) || ''); var historyItem = { price: dealPrice.amount || 0, regular: regularPrice.amount || 0, currency: dealPrice.currency || '', cut: cut, store: shopName, date: timestamp, }; allHistory.push(historyItem); // 只保留每个折扣百分比第一次出现 (seenCuts 去重, 参考项目约定) if (cut > 0 && !seenCuts[cut]) { seenCuts[cut] = true; discounts.push({ cut: cut, price: historyItem.price, regular: historyItem.regular, currency: historyItem.currency, store: shopName, date: timestamp, }); } } // 排序: 折扣记录按折扣百分比降序, 价格历史按日期降序 discounts.sort(function (a, b) { return b.cut - a.cut; }); allHistory.sort(function (a, b) { return new Date(b.date) - new Date(a.date); }); // 计算史低 (最大折扣对应的价格) var lowest = null; if (discounts.length > 0) { var maxCut = discounts[0]; lowest = { price: maxCut.price, currency: maxCut.currency, store: maxCut.store, date: maxCut.date, cut: maxCut.cut, }; } return { history: allHistory.slice(0, 20), discounts: discounts, lowest: lowest, }; } // ==================== 公共 API ==================== /** * 初始化 ITAD 客户端 * @param {Object} config * - apiKey: ITAD API Key (32 位十六进制) * - ttl: 自定义缓存 TTL (覆盖 DEFAULT_TTL) * - cachePrefix: 自定义缓存键前缀 */ function init(config) { config = config || {}; _apiKey = config.apiKey || ''; if (config.ttl) _ttl = Object.assign({}, DEFAULT_TTL, config.ttl); if (config.cachePrefix) _cachePrefix = config.cachePrefix; } function setApiKey(key) { _apiKey = key || ''; } function getApiKey() { return _apiKey; } function hasApiKey() { return !!_apiKey; } /** * 清除缓存 * @param {string} [prefix] - 只清除以指定前缀开头的缓存键 */ function clearCache(prefix) { if (typeof GM_listValues !== 'function') return; var keys = GM_listValues(); var p = _cachePrefix + (prefix || ''); for (var i = 0; i < keys.length; i++) { if (keys[i].indexOf(p) === 0) { try { GM_setValue(keys[i], ''); } catch (e) { /* ignore */ } } } } var SGLVITAD = { version: VERSION, // 常量 ITAD_BASE_URL: ITAD_BASE_URL, SHOP_STEAM: ITAD_SHOP_STEAM, DEFAULT_TTL: DEFAULT_TTL, MAX_BATCH: MAX_BATCH, // 初始化 init: init, setApiKey: setApiKey, getApiKey: getApiKey, hasApiKey: hasApiKey, clearCache: clearCache, // ID 映射 lookupItadId: lookupItadId, lookupItadIds: lookupItadIds, // 游戏信息 fetchGameInfo: fetchGameInfo, // 价格概览 (核心增强端点) fetchPriceOverview: fetchPriceOverview, // 多商店/多区域价格 fetchPrices: fetchPrices, fetchMultiRegionPrices: fetchMultiRegionPrices, // 历史价格 fetchPriceHistory: fetchPriceHistory, // 史低/单店低 fetchHistoryLow: fetchHistoryLow, fetchStoreLow: fetchStoreLow, // 工具 getShops: getShops, testConnection: testConnection, // 归一化 (暴露用于测试/自定义处理) _normalize: { overview: normalizeOverview, prices: normalizePrices, history: normalizeHistory, price: normalizePrice, }, }; root.SGLVITAD = SGLVITAD; })(typeof window !== 'undefined' ? window : globalThis);