SGLV ITAD (IsThereAnyDeal) API 封装库,统一封装 ITAD 价格相关接口。
Tento skript by neměl být instalován přímo. Jedná se o knihovnu, kterou by měly jiné skripty využívat pomocí meta příkazu // @require https://update.greasyfork.org/scripts/590418/1896925/SGLV%20ITAD%20Library.js
// ==UserScript==
// @name SGLV ITAD Library
// @namespace sglv-lib
// @version 1.0.2
// @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.2') return;
// ==================== 常量 ====================
const VERSION = '1.0.2';
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<string|null>} 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<number|string>} appIds - Steam App IDs (最多 200)
* @param {number} shopId - 商店 ID
* @returns {Promise<Object>} { "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<Object|null>} { 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<string>} itadIds - ITAD Game UUID(s), 最多 200
* @param {Object} opts - { country, shops }
* - country: ISO 3166-1 alpha-2, 默认 'US'
* - shops: 商店 ID 数组, 如 [61] 仅 Steam; 不传则全部商店
* @returns {Promise<Object|null>} 归一化后的 { 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<string>} 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<Array|null>} 归一化后的价格数组
*/
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<string>} countries - ISO 3166-1 alpha-2 国家代码数组
* @param {Object} opts - { concurrency, shops }
* @returns {Promise<Object>} { 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<Object>} 归一化后的 { 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<string>} itadIds - ITAD Game UUID(s)
* @param {Object} opts - { country }
* @returns {Promise<Object|null>} { "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<string>} itadIds - ITAD Game UUID(s)
* @param {Object} opts - { country, shops }
* @returns {Promise<Object|null>}
*/
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<Array|null>} [{ 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<Object>} { 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) {
var match = e.message.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: e.message };
}
}
// ==================== 响应归一化层 ====================
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);