// ==UserScript== // @name SGLV Core Library (SGLV-Suite) // @name:en SGLV Core Library (SGLV-Suite) // @namespace https://greasyfork.org/scripts/588485 // @version 1.0.3 // @description SGLV 核心依赖库(v1.0.3):跨脚本复用的基础设施工具集。包含 NS 命名空间、IndexedDB KV 持久化、负缓存、StageProgress 多阶段进度条、makeThrottledProgress 节流、concurrentPool 智能并发池(429 降并发 + 指数退避)、twoPhaseFetch 两阶段批量抓取、fillPendingSlots 占位、gmFetchJson/gmFetchText GM_xmlhttpRequest 包装、v1.0.3 新增 PersistentComputeCache 持久化计算缓存(IDB+内存镜像+输入签名+schema版本,支持 getOrCompute/invalidateByInput/getSync 同步预热)。供主脚本 @require 引用,不可单独使用。 // @description:en SGLV Core Library v1.0.3 — Cross-script infrastructure: NS namespace, IndexedDB KV, neg cache, StageProgress, throttled progress, smart concurrentPool (429 backoff), twoPhaseFetch, fillPendingSlots, GM fetch wrappers, **NEW v1.0.3** PersistentComputeCache (IDB+memory mirror, input signature, schema version, getOrCompute / invalidateByInput / getSync prewarm). Inspired by steam-friend-manager-1.2.2. For host script @require only. // @author hshow // @license MIT // @match *://*/* // @grant GM_xmlhttpRequest // @noframes // ==/UserScript== /* * SGLV Core Library v1.0.0 * * 设计目标:从 SGLV 业务脚本中抽出"零业务依赖"的基础设施工具, * 供所有 SGLV 系列脚本(steam-game-library-viewer / wishlist / badges / economy / family ...) * 通过 @require 共享,避免每次新脚本都重写一遍 IDB/进度/并发/负缓存。 * * API 暴露方式:window.SGLVCore (IIFE 直接挂全局,无依赖注入) * * 设计原则: * 1. 零业务依赖 — 不引入任何 Steam / GM 业务概念,纯工具 * 2. GM_* 兼容 — gmFetchJson / gmFetchText 用 GM_xmlhttpRequest(脚本侧); * 测试侧可注入 mockGmXhr 覆盖 * 3. IDB 兼容 — 全部走原生 indexedDB;测试侧可注入 mockIdb 覆盖 * 4. DOM 兼容 — StageProgress 依赖 document;测试侧用 jsdom * 5. 渐进式破坏 — API 内部 try/catch 兜底,任何一步失败不影响宿主 * * 桥接契约(测试/宿主需要注入): * window.__sglvCoreMockIdb = { DB_NAME, STORE, open, ... } 可选,默认走原生 indexedDB * window.__sglvCoreMockGmXhr = function(method, url, opts, cb) 可选,默认走 GM_xmlhttpRequest * * API: * version: '1.0.3' * apiVersion: 1 * NS: 'sglv' * nsKey(...parts): 拼接命名空间 key * IDB: { loadAll, get, set, remove, has, keys } 内存镜像 + IDB 异步写 * negCache: { get, set, clear } 通用负缓存(30min TTL,封装在调用方) * StageProgress: { ensure, setStage, setProgress, finish, fail, hide, show } * makeThrottledProgress(onProgress, throttleMs=800): 节流的 onProgress * concurrentPool(tasks, concurrency, onProgress): 智能并发池(429 降并发 + 指数退避) * twoPhaseFetch(items, fastFn, carefulFn, options): 两阶段批量抓取 * fillPendingSlots(items, max=6, pendingMarker): pending slot 占位 * gmFetchJson(url, options): GM_xmlhttpRequest JSON 包装(重试) * gmFetchText(url, options): GM_xmlhttpRequest 文本包装(重试) * ensureCacheVersions(defaultVersions): 缓存 schema 版本号升级工具 * **v1.0.3 NEW** PersistentComputeCache: { getOrCompute, getSync, has, invalidate, invalidateByInput, clear, hydrate } * 持久化计算缓存(IDB+内存镜像+输入签名+schema版本号) */ (function (root) { 'use strict'; // ==================== 1. NS 命名空间 ==================== // 所有 GM key / IDB key 统一前缀,便于重命名 / 拆分 / 合并 const NS = 'sglv'; const nsKey = (...parts) => [NS, ...parts].join('_'); // ==================== 2. IndexedDB KV 库 ==================== // 替代 GM_setValue 大对象,容量无 5MB 限制,启动更快 // 设计参考 steam-friend-manager-1.2.2:48-150 const sglvIDB = (function () { const DB_NAME = 'sglv_kv', STORE = 'kv', VERSION = 1; let _db = null; const _mem = {}; // 内存镜像:key → value let _loaded = false; const _writing = new Set(); // 正在写 IDB 的 key 集合(防抖) function open() { return new Promise((resolve, reject) => { const idb = root.indexedDB; if (!idb) { reject(new Error('indexedDB unavailable')); return; } const req = idb.open(DB_NAME, VERSION); req.onerror = () => reject(req.error); req.onsuccess = () => { _db = req.result; resolve(_db); }; req.onupgradeneeded = e => { if (!e.target.result.objectStoreNames.contains(STORE)) { e.target.result.createObjectStore(STORE); } }; }); } function loadAll() { if (_loaded) return Promise.resolve(_mem); return open().then(() => new Promise(resolve => { const tx = _db.transaction(STORE, 'readonly'); const req = tx.objectStore(STORE).openCursor(); req.onsuccess = () => { const c = req.result; if (c) { _mem[c.key] = c.value; c.continue(); } else { _loaded = true; resolve(_mem); } }; req.onerror = () => { _loaded = true; resolve(_mem); }; })); } function get(key, fallback) { return _mem[key] !== undefined ? _mem[key] : fallback; } function set(key, val) { _mem[key] = val; if (!_db) return; if (_writing.has(key)) return; // 防抖:避免重复写 _writing.add(key); const tx = _db.transaction(STORE, 'readwrite'); tx.objectStore(STORE).put(val, key); tx.oncomplete = () => _writing.delete(key); tx.onerror = () => _writing.delete(key); } function remove(key) { delete _mem[key]; if (!_db) return; const tx = _db.transaction(STORE, 'readwrite'); tx.objectStore(STORE).delete(key); } function has(key) { return _mem[key] !== undefined; } function keys() { return Object.keys(_mem); } return { loadAll, get, set, remove, has, keys }; })(); // ==================== 3. 负缓存 ==================== // 失败结果短期不再重试(TTL 由调用方控制,常见 30min) // 设计参考 steam-friend-manager fetchGameZhName 负缓存模式 const negCache = { get(key, ttlMs) { const v = sglvIDB.get(key, null); if (!v || v._ok !== false) return null; if (Date.now() - (v._ts || 0) > ttlMs) return null; return v._err || true; }, set(key, err) { sglvIDB.set(key, { _ok: false, _err: err, _ts: Date.now() }); }, clear(key) { sglvIDB.remove(key); }, }; // ==================== 4. 缓存 schema 版本号工具 ==================== // 升级时自动失效旧版本缓存(调用方传入默认 version map) // 设计参考 steam-friend-manager-1.2.2:1675-1680 MUTUAL_CACHE_VERSION const CACHE_VER_KEY = nsKey('cache_versions'); function ensureCacheVersions(defaultVersions) { const versions = defaultVersions || {}; const cur = sglvIDB.get(CACHE_VER_KEY, {}) || {}; const updates = {}; let changed = false; for (const [name, ver] of Object.entries(versions)) { if ((cur[name] || 0) < ver) { updates[name] = ver; changed = true; } } if (changed) sglvIDB.set(CACHE_VER_KEY, { ...cur, ...updates }); return updates; // 返回"已升级的 cache 名称集合" } // ==================== 4.5 v1.0.3: 持久化计算缓存 ==================== // 解决"大批量计算结果无法跨 session 复用"的问题: // - IDB 持久化:计算结果写入 IndexedDB,关闭浏览器后下次启动仍可用 // - 输入签名:每个缓存 key 关联一个"输入指纹"(games count + 关键字段 hash), // 数据源变更时自动失效(避免渲染陈旧数据) // - schema 版本:业务 schema 升级时强制全部失效 // - 同步预热:getSync() 同步返回当前内存镜像(0 延迟),启动流程可同步填充 KPI // - 细粒度失效:invalidateByInput(predicate) 按输入签名谓词批量失效 // // 典型使用: // // 1) 启动时同步预热 — 已有缓存直接填充,无则返回 null(不触发重算) // const cached = PCC.getSync('yearly_stats'); // if (cached) state.yearlyStats = cached; // // // 2) 渲染时按需计算 — 命中 IDB 跳过,未命中异步重算 // async function getYearlyStats(games) { // const sig = `${games.length}|${games.reduce((s,g)=>s+(g.acquiredTime||0),0)}`; // return PCC.getOrCompute('yearly_stats', 1, sig, () => computeYearly(games)); // } // // // 3) 数据刷新时 — 失效旧缓存 // PCC.invalidate('yearly_stats'); const PersistentComputeCache = (function () { const STORE_KEY_PREFIX = 'pcc_'; const _mem = {}; // { [key]: { _sig, _ver, value, _ts } } const _writing = new Set(); // 防抖:同 key 短时间内多次写合并 const _writeTimers = new Map(); function _strip(key) { return key.startsWith(STORE_KEY_PREFIX) ? key.slice(STORE_KEY_PREFIX.length) : key; } function _now() { return Date.now(); } // 同步获取当前缓存(已 hydrate 完成后),不触发任何异步操作 // 启动流程可同步读取,0 延迟填充 UI function getSync(key) { const entry = _mem[key]; if (!entry) return null; return entry.value; } // 同步获取带元数据({ value, _sig, _ver, _ts }),便于业务层做输入签名匹配 function getSyncWithMeta(key) { const entry = _mem[key]; if (!entry) return null; return { value: entry.value, _sig: entry._sig, _ver: entry._ver, _ts: entry._ts }; } function has(key) { return _mem[key] !== undefined; } // 同步写入内存(不落盘) — 启动 hydrate 时使用 function _setMemOnly(key, value, sig, ver) { _mem[key] = { _sig: sig, _ver: ver, value, _ts: _now() }; } // 异步落盘(防抖 100ms 合并同 key 多次写) function _flush(key) { const entry = _mem[key]; if (!entry) return; // 直接走 sglvIDB(继承其 _writing 防抖 + IDB 异步写) sglvIDB.set(STORE_KEY_PREFIX + key, { _sig: entry._sig, _ver: entry._ver, value: entry.value, _ts: entry._ts, }); } // 核心:获取或异步计算 // key: 业务唯一 key // schemaVer: 业务 schema 版本号,变更时强制重算 // inputSig: 输入签名(调用方负责计算,如 "123|456") // computeFn: 缓存未命中时执行的计算函数(同步/异步均可) // 返回:Promise async function getOrCompute(key, schemaVer, inputSig, computeFn) { // 1) 内存命中 + 输入签名一致 + schema 版本匹配 → 直接返回 const mem = _mem[key]; if (mem && mem._ver === schemaVer && mem._sig === inputSig) { return mem.value; } // 2) IDB 命中(可能 hydrate 未完成,这里读 IDB 兜底) try { const persisted = sglvIDB.get(STORE_KEY_PREFIX + key, null); if (persisted && persisted._ver === schemaVer && persisted._sig === inputSig) { _mem[key] = { _sig: inputSig, _ver: schemaVer, value: persisted.value, _ts: persisted._ts || _now() }; return persisted.value; } } catch (e) { /* IDB 读取失败,继续重算 */ } // 3) 未命中:执行计算 + 写入 const value = await computeFn(); _mem[key] = { _sig: inputSig, _ver: schemaVer, value, _ts: _now() }; // 防抖落盘(100ms 内同 key 多次写合并) if (_writeTimers.has(key)) clearTimeout(_writeTimers.get(key)); _writeTimers.set(key, setTimeout(() => { _writeTimers.delete(key); _flush(key); }, 100)); return value; } // 同步变体 — 适用于"已 hydrate 但用户同步访问"场景 // 若内存有则返回,无则同步执行 computeFn 并落盘 function getOrComputeSync(key, schemaVer, inputSig, computeFn) { const mem = _mem[key]; if (mem && mem._ver === schemaVer && mem._sig === inputSig) return mem.value; const value = computeFn(); _mem[key] = { _sig: inputSig, _ver: schemaVer, value, _ts: _now() }; if (_writeTimers.has(key)) clearTimeout(_writeTimers.get(key)); _writeTimers.set(key, setTimeout(() => { _writeTimers.delete(key); _flush(key); }, 100)); return value; } // 直接设置值(已有计算结果时,跳过 compute 步骤) function set(key, schemaVer, inputSig, value) { _mem[key] = { _sig: inputSig, _ver: schemaVer, value, _ts: _now() }; if (_writeTimers.has(key)) clearTimeout(_writeTimers.get(key)); _writeTimers.set(key, setTimeout(() => { _writeTimers.delete(key); _flush(key); }, 100)); } // 失效单个 key function invalidate(key) { delete _mem[key]; sglvIDB.remove(STORE_KEY_PREFIX + key); } // 失效所有 key function clear() { const keys = Object.keys(_mem); for (const k of keys) sglvIDB.remove(STORE_KEY_PREFIX + k); for (const k of keys) delete _mem[k]; } // 批量失效:按输入签名谓词(predicate(sig, key) => bool)选择性失效 // 例如:游戏库数据变更时,失效所有 sig 以新 ownedGames.length 为前缀的 key function invalidateByInput(predicate) { for (const k of Object.keys(_mem)) { const entry = _mem[k]; if (entry && predicate(entry._sig, k)) { delete _mem[k]; sglvIDB.remove(STORE_KEY_PREFIX + k); } } } // 启动时从 IDB hydrate 全部 PCC 缓存到内存 // 返回 Promise<{ hydrated: number }> // 注意:必须在 sglvIDB.loadAll() 之后调用,否则 _writing 防抖可能导致 set 期间被读 function hydrate() { try { // sglvIDB._mem 已 loadAll 到内存,扫描所有 pcc_ 前缀的 key 解析回来 // sglvIDB._mem 不是 sglvIDB API 暴露的,但 loadAll 时是 sglvIDB 内部 _mem // 我们通过 sglvIDB.keys() 拿到所有 key,逐个 get 即可 const keys = sglvIDB.keys(); let count = 0; for (const k of keys) { if (typeof k === 'string' && k.startsWith(STORE_KEY_PREFIX)) { const v = sglvIDB.get(k, null); if (v && v._sig != null && v._ver != null && v.value !== undefined) { _mem[_strip(k)] = { _sig: v._sig, _ver: v._ver, value: v.value, _ts: v._ts || Date.now() }; count++; } } } return { hydrated: count }; } catch (e) { console.warn('[SGLV-Core] PersistentComputeCache hydrate failed:', e); return { hydrated: 0 }; } } return { getSync, getSyncWithMeta, has, getOrCompute, getOrComputeSync, set, invalidate, invalidateByInput, clear, hydrate, _flush, _mem, // 暴露给测试 }; })(); // ==================== 5. StageProgress 多阶段进度条 ==================== // 取代单一 spinner,4 阶段状态机:库存 → 详情 → 补充数据 → 完成 // 设计参考 steam-friend-manager-1.2.2:8174 dashSetStage const StageProgress = { _el: null, _stage: 0, _totalStages: 0, _text: '', _pct: 0, ensure() { if (this._el) return; const doc = root.document; if (!doc) return; this._el = doc.createElement('div'); this._el.id = 'sglv-stage-progress'; this._el.innerHTML = `
`; const panel = doc.getElementById('sglv-panel') || doc.body; const header = panel.querySelector ? panel.querySelector('.sglv-panel-header') : null; if (header && header.parentNode) header.parentNode.insertBefore(this._el, header.nextSibling); else panel.appendChild(this._el); }, setStage(n, total, text) { this.ensure(); this._stage = n; this._totalStages = total; this._text = text || ''; // 阶段切换时跳到该阶段起始百分比 this._pct = total > 1 ? Math.max(this._pct, ((n - 1) / total) * 100) : this._pct; this._render(); }, setProgress(pct, text) { this.ensure(); this._pct = Math.min(100, Math.max(0, pct)); if (text != null) this._text = text; this._render(); }, finish(text) { this.setProgress(100, text || '完成'); setTimeout(() => { if (this._el) this._el.style.display = 'none'; }, 1500); }, fail(text) { this.setProgress(0, '失败: ' + text); }, hide() { if (this._el) this._el.style.display = 'none'; }, show() { this.ensure(); if (this._el) this._el.style.display = ''; }, _render() { if (!this._el) return; const fill = this._el.querySelector('.sglv-stage-progress-fill'); const textEl = this._el.querySelector('.sglv-stage-progress-text'); if (fill) fill.style.width = this._pct + '%'; const stagePrefix = this._totalStages > 1 ? `[${this._stage}/${this._totalStages}] ` : ''; if (textEl) textEl.textContent = `${stagePrefix}${this._text} (${Math.round(this._pct)}%)`; } }; // ==================== 6. 节流 onProgress ==================== // 批量补全时主线程不被压垮,默认 800ms 节流 // 设计参考 steam-friend-manager-1.2.2:40 RENDER_THROTTLE_MS = 800 function makeThrottledProgress(onProgress, throttleMs = 800) { if (!onProgress) return null; let last = 0; let pendingArgs = null; let timer = null; const flush = () => { timer = null; if (pendingArgs) { onProgress(...pendingArgs); last = Date.now(); pendingArgs = null; } }; return function (...args) { const now = Date.now(); if (now - last >= throttleMs) { if (timer) { clearTimeout(timer); timer = null; } last = now; onProgress(...args); } else { pendingArgs = args; if (!timer) timer = setTimeout(flush, throttleMs - (now - last)); } }; } // ==================== 7. 智能并发池 ==================== // 滑动窗口模式,完成一个立刻填入下一个,保持并发数恒定 // 遇到 429 或网络错误自动指数退避重试(最多 2 次:1s→2s),429 时自动降低并发数 // 设计参考 steam-friend-manager _concurrentPool // // 契约:task() throw 视为 rejected,return 任意值视为 fulfilled // results[i] = { index, status: 'fulfilled' | 'rejected', value?, reason? } async function concurrentPool(tasks, concurrency, onProgress) { const results = new Array(tasks.length); let nextIndex = 0; let completed = 0; let activeConcurrency = concurrency; const MAX_RETRY = 2; async function runTask(index) { const task = tasks[index]; let retryCount = 0; while (retryCount <= MAX_RETRY) { try { const result = await task(); results[index] = { index, status: 'fulfilled', value: result }; return; } catch (e) { retryCount++; if (retryCount <= MAX_RETRY) { const delayMs = retryCount === 1 ? 1000 : 2000; await new Promise(r => setTimeout(r, delayMs)); if (e.message && (e.message.includes('429') || e.message.includes('rate'))) { activeConcurrency = Math.max(3, Math.floor(activeConcurrency * 0.7)); } } else { results[index] = { index, status: 'rejected', reason: e }; return; } } } } async function worker() { while (nextIndex < tasks.length) { const idx = nextIndex++; await runTask(idx); completed++; if (onProgress) onProgress(completed, tasks.length); } } const workerCount = Math.min(activeConcurrency, tasks.length); await Promise.all(Array.from({ length: workerCount }, () => worker())); return results; } // ==================== 8. 两阶段批量抓取 ==================== // 快查失败 → 精查重试,适用 Steam API、商店页抓取等限流场景 // 设计参考 steam-friend-manager-1.2.2:1707-1793 共同好友两阶段查询 // // 契约:concurrentPool 把 task() throw 视为 rejected,return 任意值视为 fulfilled。 // 本函数 task 失败时 throw,成功时 return { item, value },在 results 里读 r.value。 // failedItems 用 {item, origIndex} 形式,保留原 items 索引以便精查失败时回查。 async function twoPhaseFetch(items, fastFn, carefulFn, options = {}) { const { concurrency = 8, fastTimeout = 3000, carefulTimeout = 8000, onProgress, getKey } = options; const total = items.length; if (total === 0) return { success: [], failed: [] }; const keyOf = getKey || ((x) => x); // 阶段1:快查 const fastResults = await concurrentPool( items.map((item, i) => async () => { const value = await Promise.race([ fastFn(item), new Promise((_, r) => setTimeout(() => r(new Error('fast-timeout')), fastTimeout)) ]); return { item, origIndex: i, value }; }), concurrency, onProgress ? makeThrottledProgress((done, _t) => onProgress(done, total * 2, '快查')) : null ); const success = []; const failedItems = []; // [{ item, origIndex }] fastResults.forEach(r => { if (r.status === 'fulfilled') { const { item, value } = r.value; success.push({ key: keyOf(item), item, value }); } else { failedItems.push({ item: items[r.index], origIndex: r.index }); } }); if (failedItems.length === 0) { if (onProgress) onProgress(total, total * 2, '快查完成'); return { success, failed: [] }; } // 阶段2:精查(对失败项,降并发避免再次限流) const carefulResults = await concurrentPool( failedItems.map(fi => async () => { const value = await Promise.race([ carefulFn(fi.item), new Promise((_, r) => setTimeout(() => r(new Error('careful-timeout')), carefulTimeout)) ]); return { item: fi.item, origIndex: fi.origIndex, value }; }), Math.max(3, Math.floor(concurrency / 2)), onProgress ? makeThrottledProgress((done, _t) => onProgress(total + done, total * 2, '精查')) : null ); const careSuccess = []; const careFailed = []; carefulResults.forEach(r => { if (r.status === 'fulfilled') { const { item, value } = r.value; careSuccess.push({ key: keyOf(item), item, value }); } else { careFailed.push({ key: keyOf(items[r.index]), item: items[r.index], error: r.reason }); } }); if (onProgress) onProgress(total * 2, total * 2, '完成'); return { success: [...success, ...careSuccess], failed: careFailed }; } // ==================== 9. fillPendingSlots 占位 ==================== // pending slot 设计,空位也占位,布局稳定 // 设计参考 steam-friend-manager-1.2.2:6385 function fillPendingSlots(items, max = 6, pendingMarker = (i) => ({ pending: true, slot: i })) { const out = (items || []).slice(0, max); for (let i = out.length; i < max; i++) out.push(pendingMarker(i)); return out; } // ==================== 10. GM_xmlhttpRequest 包装 ==================== // JSON 包装:自动 parse + 失败重试 // 测试侧可通过 root.__sglvCoreMockGmXhr 覆盖 function gmFetchJson(url, options = {}) { const timeout = options.timeout || 20000; const maxRetries = options.retries || 2; const method = options.method || 'GET'; const headers = options.headers || null; const data = options.data || null; const gmxhr = root.__sglvCoreMockGmXhr || (typeof GM_xmlhttpRequest !== 'undefined' ? GM_xmlhttpRequest : null); return new Promise((resolve, reject) => { if (!gmxhr) { reject(new Error('GM_xmlhttpRequest unavailable')); return; } let attempt = 0; function doRequest() { const opts = { method, url, timeout, onload(r) { // v1.0.2: 增加 HTTP 状态码校验,避免对错误响应体(404/429/500)做无效 JSON 解析 if (r.status < 200 || r.status >= 300) { reject(new Error('HTTP ' + r.status)); return; } try { resolve(JSON.parse(r.responseText)); } catch (e) { reject(new Error('JSON parse failed: ' + e.message)); } }, onerror(err) { attempt++; if (attempt <= maxRetries) setTimeout(doRequest, attempt * 1000); else reject(err); }, ontimeout() { attempt++; if (attempt <= maxRetries) setTimeout(doRequest, attempt * 1000); else reject(new Error('timeout')); } }; if (headers) opts.headers = headers; if (data) opts.data = data; gmxhr(opts); } doRequest(); }); } // 文本包装:返回 raw text(大 JSON 场景避免主线程 parse) function gmFetchText(url, options = {}) { const timeout = options.timeout || 20000; const maxRetries = options.retries || 2; const gmxhr = root.__sglvCoreMockGmXhr || (typeof GM_xmlhttpRequest !== 'undefined' ? GM_xmlhttpRequest : null); return new Promise((resolve, reject) => { if (!gmxhr) { reject(new Error('GM_xmlhttpRequest unavailable')); return; } let attempt = 0; function doRequest() { gmxhr({ method: 'GET', url, timeout, onload(r) { if (r.status >= 200 && r.status < 300 && r.responseText) resolve(r.responseText); else reject(new Error('HTTP ' + r.status)); }, onerror(err) { attempt++; if (attempt <= maxRetries) setTimeout(doRequest, attempt * 1000); else reject(err); }, ontimeout() { attempt++; if (attempt <= maxRetries) setTimeout(doRequest, attempt * 1000); else reject(new Error('timeout')); } }); } doRequest(); }); } // ==================== 暴露 API ==================== const api = { version: '1.0.3', apiVersion: 1, NS, nsKey, IDB: sglvIDB, negCache, ensureCacheVersions, StageProgress, makeThrottledProgress, concurrentPool, twoPhaseFetch, fillPendingSlots, gmFetchJson, gmFetchText, PersistentComputeCache, // v1.0.3: 持久化计算缓存 }; // 暴露到全局(GM 脚本风格) root.SGLVCore = api; // 同时挂 unsafeWindow(某些 sandbox 环境) if (typeof unsafeWindow !== 'undefined' && unsafeWindow !== root) { try { unsafeWindow.SGLVCore = api; } catch (e) { /* ignore */ } } return api; })(typeof window !== 'undefined' ? window : globalThis);