From f14d5c6bd13681322504409416c8d4bd3df96d0d Mon Sep 17 00:00:00 2001 From: XXhaos Date: Sat, 18 Apr 2026 13:03:25 +0800 Subject: [PATCH] Update capture_gamertag.js --- Scripts/capture_gamertag.js | 282 +++++++++++++++++++++++++++--------- 1 file changed, 217 insertions(+), 65 deletions(-) diff --git a/Scripts/capture_gamertag.js b/Scripts/capture_gamertag.js index 5d3fb54..7c6a8e7 100644 --- a/Scripts/capture_gamertag.js +++ b/Scripts/capture_gamertag.js @@ -1,83 +1,235 @@ /** - * Surge 脚本:捕获 peoplehub 响应中的 gamertag + * Xbox Cart History Switcher * * 功能: - * - 保持原有:维护最新 gamertag 到 $persistentStore.gamertag - * - 新增:把每次捕获追加到 gamertag_records 数组(相邻相同值去重) + * 1. 访问 https://carthistory.com/ → 按时间戳最近邻匹配 cart 和 gamertag,展示卡片 + * 2. 点击切换 → 覆盖 $persistentStore 中的 cartId / authorization / gamertag * - * gamertag_records 结构:[{gamertag, ts}, ...] - * - 匹配由网页脚本动态完成,本脚本不做任何配对 - * - * Surge 配置: - * [Script] - * capture_gamertag = type=http-response, pattern=^https:\/\/peoplehub-public\.xboxlive\.com\/people\/gt\(.+\), requires-body=true, script-path=xxx.js - * - * [MITM] - * hostname = %APPEND% peoplehub-public.xboxlive.com + * 匹配策略(核心): + * - 读取 cart_records 和 gamertag_records 两个数组 + * - 对每条 cart,在 gamertag_records 中找时间戳绝对差最小的一条 + * - 若最小差值超过 MATCH_WINDOW_MS,则该 cart 的 gamertag 显示为 "(未知)" + * - 不在存储里记录"已匹配的三元组",每次页面刷新都重新计算 */ -const peoplePattern = /^https:\/\/peoplehub-public\.xboxlive\.com\/people\/gt\(.+\)/; +const CART_KEY = "cartId"; +const AUTH_KEY = "authorization"; +const GAMERTAG_KEY = "gamertag"; +const CART_RECORDS = "cart_records"; +const GAMERTAG_RECORDS = "gamertag_records"; + +// 匹配窗口:cart 和 gamertag 时间差超过此值就认为无法配对 +const MATCH_WINDOW_MS = 60000; // 60 秒,按需调整 + const url = $request.url; +const parsed = new URL(url); +const action = parsed.searchParams.get("action"); +const indexStr = parsed.searchParams.get("index"); -const MAX_RECORDS = 20; // gamertag 记录保留最近 20 条 +if (action === "apply" && indexStr !== null) { + applySwitch(parseInt(indexStr, 10)); +} else { + showList(); +} -if (peoplePattern.test(url)) { - if (!$response.body) { - console.log("peoplehub triggered but no response body, skip"); - } else { - try { - const body = JSON.parse($response.body); - const gamertag = body && body.people && body.people[0] && body.people[0].gamertag; +// ==================== 读取 ==================== +function readRecords(key) { + const raw = $persistentStore.read(key); + if (!raw) return []; + try { + const arr = JSON.parse(raw); + return Array.isArray(arr) ? arr : []; + } catch (e) { + return []; + } +} - if (!gamertag) { - console.log("[gamertag] 响应中未找到 gamertag,跳过"); - } else { - const now = Date.now(); +// ==================== 最近邻匹配 ==================== +function matchCartToGamertag(cart, gamertagRecords) { + if (!gamertagRecords.length) return { gamertag: "(未知)", diff: null }; - // 更新 gamertag 主 key(保持原有行为) - if (gamertag !== $persistentStore.read("gamertag")) { - $persistentStore.write(gamertag, "gamertag"); - console.log(`Stored gamertag: ${gamertag}`); - $notification.post( - "Surge 信息存储", - "已捕获 gamertag", - `gamertag: ${gamertag}` - ); - } - - // 追加到 gamertag_records - appendGamertagRecord({ gamertag, ts: now }); - } - } catch (error) { - console.log(`Error (gamertag): ${error}`); - $notification.post("Surge 脚本错误", "gamertag 捕获失败", `${error}`); + let best = null; + let bestDiff = Infinity; + for (const g of gamertagRecords) { + const diff = Math.abs(cart.ts - g.ts); + if (diff < bestDiff) { + bestDiff = diff; + best = g; } } + + if (bestDiff > MATCH_WINDOW_MS) { + return { gamertag: "(未知)", diff: bestDiff }; + } + return { gamertag: best.gamertag, diff: bestDiff }; } -function appendGamertagRecord(entry) { - let records = []; - const raw = $persistentStore.read("gamertag_records"); - if (raw) { - try { - const parsed = JSON.parse(raw); - if (Array.isArray(parsed)) records = parsed; - } catch (e) { records = []; } +// ==================== 工具函数 ==================== +function formatTimestamp(ms) { + try { + const d = new Date(ms); + const pad = n => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; + } catch (e) { + return String(ms); } - - // 相邻去重:如果最后一条就是同一个 gamertag,只更新它的 ts,不新增 - // 这样反复刷新同账号不会产生大量冗余记录 - if (records.length > 0 && records[records.length - 1].gamertag === entry.gamertag) { - records[records.length - 1].ts = entry.ts; - $persistentStore.write(JSON.stringify(records), "gamertag_records"); - console.log(`[gamertag] 更新末条时间戳: ${entry.gamertag}`); - return; - } - - records.push(entry); - if (records.length > MAX_RECORDS) records = records.slice(-MAX_RECORDS); - $persistentStore.write(JSON.stringify(records), "gamertag_records"); - console.log(`[gamertag] ✅ 新增记录: ${entry.gamertag}, total=${records.length}`); } -$done({}); +function escapeHtml(s) { + if (s == null) return ''; + return String(s).replace(/[&<>"']/g, c => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' + }[c])); +} + +function respondHtml(body) { + $done({ + response: { + status: 200, + headers: { + "Content-Type": "text/html;charset=utf-8", + "Cache-Control": "no-store, no-cache, must-revalidate", + "Pragma": "no-cache" + }, + body: body + } + }); +} + +// ==================== 展示列表 ==================== +function showList() { + const cartRecords = readRecords(CART_RECORDS); + const gamertagRecords = readRecords(GAMERTAG_RECORDS); + const currentCartId = $persistentStore.read(CART_KEY) || ""; + + // 为每条 cart 动态匹配 gamertag,生成展示数据 + const display = cartRecords.map((cart, i) => { + const match = matchCartToGamertag(cart, gamertagRecords); + return { + originalIndex: i, + cartId: cart.cartId, + authorization: cart.authorization, + ts: cart.ts, + gamertag: match.gamertag, + matchDiff: match.diff + }; + }); + + // 倒序(最新在前) + const reversed = [...display].reverse(); + + const cards = reversed.map(d => { + const isActive = d.cartId === currentCartId; + const cardStyle = isActive + ? 'border:2px solid #10b981; background:#f0fdf4;' + : 'border:1px solid #e5e7eb; background:#fff;'; + const activeBadge = isActive + ? '当前激活' + : ''; + const button = isActive + ? '' + : ``; + + return ` +
+
+ ${escapeHtml(d.gamertag)}${activeBadge} +
+
+ ${escapeHtml(formatTimestamp(d.ts))} +
+ ${button} +
`; + }).join(''); + + const emptyHint = cartRecords.length === 0 + ? '
暂无 cart 记录
' + : ''; + + const html = ` + + + + +Cart History + + +
+

🛒 Cart 账号历史

+
共 ${cartRecords.length} 条记录(最新在前)
+ ${cards} + ${emptyHint} +
+ +`; + + respondHtml(html); +} + +// ==================== 执行切换 ==================== +function applySwitch(index) { + const cartRecords = readRecords(CART_RECORDS); + const gamertagRecords = readRecords(GAMERTAG_RECORDS); + + if (!Number.isInteger(index) || index < 0 || index >= cartRecords.length) { + return showError("无效的记录索引", `index=${index}, 记录数=${cartRecords.length}`); + } + + const cart = cartRecords[index]; + if (!cart || !cart.cartId || !cart.authorization) { + return showError("记录不完整", "该条记录缺少 cartId 或 authorization"); + } + + const match = matchCartToGamertag(cart, gamertagRecords); + const gamertag = match.gamertag; + + // 覆盖 $persistentStore + $persistentStore.write(cart.cartId, CART_KEY); + $persistentStore.write(cart.authorization, AUTH_KEY); + if (gamertag && gamertag !== "(未知)") { + $persistentStore.write(gamertag, GAMERTAG_KEY); + } + + $notification.post( + "✅ 账号切换成功", + `已切换到 ${gamertag}`, + "" + ); + + const html = ` + + + + +切换成功 + + + +
+
+
切换成功
+
已切换到 ${escapeHtml(gamertag)}
+
1 秒后自动返回列表...
+
+ +`; + + respondHtml(html); +} + +// ==================== 错误页 ==================== +function showError(title, detail) { + const html = ` + +错误 + +
+
+
${escapeHtml(title)}
+
${escapeHtml(detail)}
+ 返回列表 +
+ +`; + respondHtml(html); +}