209 lines
8.5 KiB
JavaScript
209 lines
8.5 KiB
JavaScript
const readline = require('readline');
|
||
|
||
const rl = readline.createInterface({
|
||
input: process.stdin,
|
||
output: process.stdout
|
||
});
|
||
|
||
// 终极破甲器:手动追踪跳转,继承 Cookie,半路截取游戏 ID
|
||
async function resolveGameId(startUrl) {
|
||
let currentUrl = startUrl;
|
||
let cookies = {};
|
||
|
||
for (let i = 0; i < 7; i++) { // 最多追踪 7 层跳转
|
||
try {
|
||
// 拼接继承的 Cookie
|
||
const cookieHeader = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ');
|
||
|
||
const response = await fetch(currentUrl, {
|
||
redirect: 'manual', // 关键:关闭自动跳转,改为我们手动一步步跟
|
||
headers: {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||
'Accept-Language': 'en-US,en;q=0.5',
|
||
'Cookie': cookieHeader
|
||
}
|
||
});
|
||
|
||
// 继承服务器发下来的 Cookie,伪装得更像真人
|
||
const setCookieHeader = response.headers.get('set-cookie');
|
||
if (setCookieHeader) {
|
||
const parts = setCookieHeader.split(/,(?=\s*[a-zA-Z0-9_-]+\s*=)/);
|
||
for (const part of parts) {
|
||
const cookiePair = part.split(';')[0];
|
||
const [key, ...val] = cookiePair.split('=');
|
||
if (key && val) cookies[key.trim()] = val.join('=').trim();
|
||
}
|
||
}
|
||
|
||
if (response.status >= 300 && response.status < 400) {
|
||
const location = response.headers.get('location');
|
||
if (!location) break;
|
||
|
||
const nextUrl = location.startsWith('http') ? location : new URL(location, currentUrl).href;
|
||
|
||
// 解密 URL,剥开追踪网的层层包装
|
||
let decodedUrl = nextUrl;
|
||
try { decodedUrl = decodeURIComponent(decodedUrl); } catch(e){}
|
||
try { decodedUrl = decodeURIComponent(decodedUrl); } catch(e){}
|
||
|
||
// ⭐️ 半路截胡:只要在跳转链接里发现了 9 开头的 12 位代码,直接带走,不再往下跳!
|
||
const idMatch = decodedUrl.match(/(?:\/|id=|ProductId=|bigIds=)([9][A-Za-z0-9]{11})(?:[\/?#&'"]|$)/i);
|
||
if (idMatch) return idMatch[1].toUpperCase();
|
||
|
||
currentUrl = nextUrl;
|
||
} else if (response.status === 200) {
|
||
const htmlText = await response.text();
|
||
|
||
// 搜刮网页源码,防备 JS 动态跳转
|
||
const htmlMatch = htmlText.match(/(?:\/|id=|ProductId=|bigIds=)([9][A-Za-z0-9]{11})(?:[\/?#&'"]|$)/i);
|
||
if (htmlMatch) return htmlMatch[1].toUpperCase();
|
||
|
||
// 检查 Meta Refresh 自动跳转
|
||
const metaRefresh = htmlText.match(/<meta[^>]*http-equiv=["']refresh["'][^>]*content=["']\d+;\s*url=([^"']+)["']/i);
|
||
if (metaRefresh) {
|
||
currentUrl = metaRefresh[1].replace(/&/g, '&');
|
||
if (!currentUrl.startsWith('http')) currentUrl = new URL(currentUrl, startUrl).href;
|
||
continue;
|
||
}
|
||
|
||
// 检查 JS 自动跳转
|
||
const jsRedirect = htmlText.match(/(?:window\.)?location(?:\.href)?\s*=\s*['"]([^'"]+)['"]/i);
|
||
if (jsRedirect) {
|
||
currentUrl = jsRedirect[1];
|
||
if (!currentUrl.startsWith('http')) currentUrl = new URL(currentUrl, startUrl).href;
|
||
continue;
|
||
}
|
||
|
||
break;
|
||
} else {
|
||
break;
|
||
}
|
||
} catch (e) {
|
||
break;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// 核心查询逻辑
|
||
async function getUSGameData(startUrl) {
|
||
try {
|
||
const urlObj = new URL(startUrl);
|
||
urlObj.searchParams.set('r', 'en-us');
|
||
|
||
let bigId = await resolveGameId(urlObj.toString());
|
||
|
||
if (!bigId) {
|
||
return { success: false, reason: "防爬虫拦截,未能从底层剥离出 12 位游戏代码" };
|
||
}
|
||
|
||
const apiUrl = `https://displaycatalog.mp.microsoft.com/v7.0/products?bigIds=${bigId}&market=US&languages=en-us&MS-CV=DUMMY.1`;
|
||
|
||
const apiResponse = await fetch(apiUrl);
|
||
const data = await apiResponse.json();
|
||
|
||
if (!data.Products || data.Products.length === 0) {
|
||
return { success: false, reason: `成功获取 ID (${bigId}),但美区查无此游戏数据` };
|
||
}
|
||
|
||
const product = data.Products[0];
|
||
const gameName = product.LocalizedProperties?.[0]?.ProductTitle || "未知游戏";
|
||
|
||
let finalPrice = null;
|
||
|
||
if (!product.DisplaySkuAvailabilities || product.DisplaySkuAvailabilities.length === 0) {
|
||
return { success: false, name: gameName, reason: "该游戏没有销售规格 (无法购买)" };
|
||
}
|
||
|
||
// 智能找买断价
|
||
for (const skuObj of product.DisplaySkuAvailabilities) {
|
||
if (skuObj.Sku && (skuObj.Sku.SkuType === 'full' || skuObj.Sku.SkuType === 'dlc' || skuObj.Sku.SkuType === 'consumable')) {
|
||
for (const avail of skuObj.Availabilities || []) {
|
||
if (avail.Actions && avail.Actions.includes('Purchase') && avail.OrderManagementData?.Price !== undefined) {
|
||
finalPrice = avail.OrderManagementData.Price.ListPrice;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (finalPrice !== null) break;
|
||
}
|
||
|
||
if (finalPrice === null) {
|
||
for (const skuObj of product.DisplaySkuAvailabilities) {
|
||
for (const avail of skuObj.Availabilities || []) {
|
||
if (avail.Actions && avail.Actions.includes('Purchase') && avail.OrderManagementData?.Price !== undefined) {
|
||
finalPrice = avail.OrderManagementData.Price.ListPrice;
|
||
break;
|
||
}
|
||
}
|
||
if (finalPrice !== null) break;
|
||
}
|
||
}
|
||
|
||
if (finalPrice === null) {
|
||
return { success: false, name: gameName, reason: "只有 XGP 订阅试玩或捆绑包专属,无单买价格" };
|
||
}
|
||
|
||
return { success: true, name: gameName, price: finalPrice };
|
||
} catch (e) {
|
||
return { success: false, reason: `发生异常: ${e.message}` };
|
||
}
|
||
}
|
||
|
||
const inputUrls = [];
|
||
|
||
console.log('🎮 请粘贴你的 Xbox 链接串 (支持包含回车的多行文本)。');
|
||
console.log('💡 提示:粘贴完成后,请在【新的一行】按一次回车开始计算:\n');
|
||
|
||
rl.on('line', (line) => {
|
||
const trimmedLine = line.trim();
|
||
if (trimmedLine === '') {
|
||
if (inputUrls.length > 0) {
|
||
rl.close();
|
||
processUrls(inputUrls);
|
||
}
|
||
return;
|
||
}
|
||
const splitUrls = trimmedLine.split(/(?=https?:\/\/)/).filter(u => u.startsWith('http'));
|
||
inputUrls.push(...splitUrls);
|
||
});
|
||
|
||
async function processUrls(urls) {
|
||
console.log(`\n✅ 成功读取到 ${urls.length} 个链接,开始逐个查询美区价格...\n`);
|
||
|
||
let totalPrice = 0;
|
||
let successCount = 0;
|
||
const failedDetails = [];
|
||
|
||
for (let i = 0; i < urls.length; i++) {
|
||
const url = urls[i];
|
||
process.stdout.write(`[${i + 1}/${urls.length}] 正在查询... `);
|
||
|
||
const result = await getUSGameData(url);
|
||
|
||
if (result.success) {
|
||
console.log(`✅ 成功 | ${result.name} | 现价: $${result.price}`);
|
||
totalPrice += result.price;
|
||
successCount++;
|
||
} else {
|
||
const namePart = result.name ? `[${result.name}] ` : "";
|
||
console.log(`❌ 失败 | ${namePart}原因: ${result.reason}`);
|
||
failedDetails.push({ url, reason: result.reason, name: result.name });
|
||
}
|
||
}
|
||
|
||
console.log("\n================ 结算单 ================");
|
||
console.log(`总计识别: ${urls.length} 个游戏`);
|
||
console.log(`成功查询: ${successCount} 个游戏`);
|
||
console.log(`美元总价: $${totalPrice.toFixed(2)}`);
|
||
console.log("========================================\n");
|
||
|
||
if (failedDetails.length > 0) {
|
||
console.log("⚠️ 以下链接需要手动核查:");
|
||
failedDetails.forEach((f, idx) => {
|
||
const nameStr = f.name ? `游戏: ${f.name}\n ` : "";
|
||
console.log(`${idx + 1}. ${nameStr}原因: ${f.reason}\n 链接: ${f.url}`);
|
||
});
|
||
}
|
||
} |