Update capture_gamertag.js

This commit is contained in:
XXhaos
2026-04-18 13:05:56 +08:00
committed by GitHub
parent b7d48ccfe0
commit 6852b2fd5f

View File

@@ -1,235 +1,83 @@
/** /**
* Xbox Cart History Switcher * Surge 脚本:捕获 peoplehub 响应中的 gamertag
* *
* 功能: * 功能:
* 1. 访问 https://carthistory.com/ → 按时间戳最近邻匹配 cart 和 gamertag,展示卡片 * - 保持原有:维护最新 gamertag 到 $persistentStore.gamertag
* 2. 点击切换 → 覆盖 $persistentStore 中的 cartId / authorization / gamertag * - 新增:把每次捕获追加到 gamertag_records 数组(相邻相同值去重)
* *
* 匹配策略(核心): * gamertag_records 结构:[{gamertag, ts}, ...]
* - 读取 cart_records 和 gamertag_records 两个数组 * - 匹配由网页脚本动态完成,本脚本不做任何配对
* - 对每条 cart在 gamertag_records 中找时间戳绝对差最小的一条 *
* - 若最小差值超过 MATCH_WINDOW_MS则该 cart 的 gamertag 显示为 "(未知)" * 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
*/ */
const CART_KEY = "cartId"; const peoplePattern = /^https:\/\/peoplehub-public\.xboxlive\.com\/people\/gt\(.+\)/;
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 url = $request.url;
const parsed = new URL(url);
const action = parsed.searchParams.get("action");
const indexStr = parsed.searchParams.get("index");
if (action === "apply" && indexStr !== null) { const MAX_RECORDS = 20; // gamertag 记录保留最近 20 条
applySwitch(parseInt(indexStr, 10));
if (peoplePattern.test(url)) {
if (!$response.body) {
console.log("peoplehub triggered but no response body, skip");
} else { } else {
showList();
}
// ==================== 读取 ====================
function readRecords(key) {
const raw = $persistentStore.read(key);
if (!raw) return [];
try { try {
const arr = JSON.parse(raw); const body = JSON.parse($response.body);
return Array.isArray(arr) ? arr : []; const gamertag = body && body.people && body.people[0] && body.people[0].gamertag;
} catch (e) {
return [];
}
}
// ==================== 最近邻匹配 ==================== if (!gamertag) {
function matchCartToGamertag(cart, gamertagRecords) { console.log("[gamertag] 响应中未找到 gamertag,跳过");
if (!gamertagRecords.length) return { gamertag: "(未知)", diff: null }; } else {
const now = Date.now();
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 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);
}
}
function escapeHtml(s) {
if (s == null) return '';
return String(s).replace(/[&<>"']/g, c => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[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
? '<span style="background:#10b981; color:#fff; padding:2px 8px; border-radius:4px; font-size:12px; margin-left:8px; vertical-align:middle;">当前激活</span>'
: '';
const button = isActive
? '<button disabled style="padding:8px 16px; background:#d1d5db; color:#6b7280; border:none; border-radius:6px; font-weight:bold; cursor:not-allowed;">已激活</button>'
: `<a href="https://carthistory.com/?action=apply&index=${d.originalIndex}" style="text-decoration:none;"><button style="padding:8px 16px; background:#3b82f6; color:#fff; border:none; border-radius:6px; font-weight:bold; cursor:pointer;">切换到此账号</button></a>`;
return `
<div style="${cardStyle} border-radius:8px; padding:16px; margin-bottom:12px;">
<div style="font-size:18px; font-weight:bold; color:#111827;">
${escapeHtml(d.gamertag)}${activeBadge}
</div>
<div style="font-size:13px; color:#6b7280; margin:6px 0 12px 0;">
${escapeHtml(formatTimestamp(d.ts))}
</div>
${button}
</div>`;
}).join('');
const emptyHint = cartRecords.length === 0
? '<div style="text-align:center; color:#6b7280; padding:40px; background:#fff; border-radius:8px;">暂无 cart 记录</div>'
: '';
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cart History</title>
</head>
<body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; padding:20px; margin:0; background:#f3f4f6;">
<div style="max-width:600px; margin:0 auto;">
<h2 style="margin:0 0 20px 0; color:#111827;">🛒 Cart 账号历史</h2>
<div style="margin-bottom:12px; color:#6b7280; font-size:14px;">共 ${cartRecords.length} 条记录(最新在前)</div>
${cards}
${emptyHint}
</div>
</body>
</html>`;
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);
}
// 更新 gamertag 主 key保持原有行为
if (gamertag !== $persistentStore.read("gamertag")) {
$persistentStore.write(gamertag, "gamertag");
console.log(`Stored gamertag: ${gamertag}`);
$notification.post( $notification.post(
"✅ 账号切换成功", "Surge 信息存储",
`已切换到 ${gamertag}`, "已捕获 gamertag",
"" `gamertag: ${gamertag}`
); );
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>切换成功</title>
<meta http-equiv="refresh" content="1; url=https://carthistory.com/">
</head>
<body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; padding:20px; background:#f3f4f6;">
<div style="max-width:500px; margin:80px auto; background:#fff; border-radius:12px; padding:32px; text-align:center; box-shadow:0 4px 6px rgba(0,0,0,0.1);">
<div style="font-size:48px; margin-bottom:16px;">✅</div>
<div style="font-size:20px; font-weight:bold; color:#10b981; margin-bottom:8px;">切换成功</div>
<div style="font-size:16px; color:#374151;">已切换到 <b>${escapeHtml(gamertag)}</b></div>
<div style="font-size:13px; color:#6b7280; margin-top:16px;">1 秒后自动返回列表...</div>
</div>
</body>
</html>`;
respondHtml(html);
} }
// ==================== 错误页 ==================== // 追加到 gamertag_records
function showError(title, detail) { appendGamertagRecord({ gamertag, ts: now });
const html = `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>错误</title></head>
<body style="font-family:-apple-system,BlinkMacSystemFont,sans-serif; padding:20px; background:#f3f4f6;">
<div style="max-width:500px; margin:80px auto; background:#fff; border-radius:12px; padding:32px; text-align:center;">
<div style="font-size:48px; margin-bottom:16px;">❌</div>
<div style="font-size:20px; font-weight:bold; color:#ef4444; margin-bottom:8px;">${escapeHtml(title)}</div>
<div style="font-size:14px; color:#6b7280; margin-bottom:16px;">${escapeHtml(detail)}</div>
<a href="https://carthistory.com/" style="color:#3b82f6;">返回列表</a>
</div>
</body>
</html>`;
respondHtml(html);
} }
} catch (error) {
console.log(`Error (gamertag): ${error}`);
$notification.post("Surge 脚本错误", "gamertag 捕获失败", `${error}`);
}
}
}
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 = []; }
}
// 相邻去重:如果最后一条就是同一个 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({});