更新 Scripts/AddMsGames.js
This commit is contained in:
@@ -9,20 +9,8 @@
|
|||||||
* - 访问 https://addmsgames.com/?region=US → 直接执行美区加购
|
* - 访问 https://addmsgames.com/?region=US → 直接执行美区加购
|
||||||
* - 访问 https://addmsgames.com/?region=NG → 直接执行尼区加购
|
* - 访问 https://addmsgames.com/?region=NG → 直接执行尼区加购
|
||||||
* - 访问 https://addmsgames.com/?region=AR → 直接执行阿区加购
|
* - 访问 https://addmsgames.com/?region=AR → 直接执行阿区加购
|
||||||
*
|
|
||||||
* PersistentStore Key 说明(三区共用,与 NewAddToCart_Web.js 完全一致):
|
|
||||||
* - MUID: cart-x-authorization-muid
|
|
||||||
* - CV: cart-ms-cv
|
|
||||||
* - 列表: XboxProductList
|
|
||||||
*
|
|
||||||
* 流程:
|
|
||||||
* 1. GET 读取远程当前组(服务端加锁)
|
|
||||||
* 2. 执行加购
|
|
||||||
* 3. POST /surge/commit 提交结果
|
|
||||||
* 4. 若远程无数据,回退到本地对应区域的 XboxProductList-{REGION}
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ========================= 区域配置 =========================
|
|
||||||
const REGION_CONFIGS = {
|
const REGION_CONFIGS = {
|
||||||
US: {
|
US: {
|
||||||
label: "美区",
|
label: "美区",
|
||||||
@@ -62,12 +50,11 @@ const REGION_CONFIGS = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const REMOTE_READ_URL = 'https://xbox-bot.biubiubiu-lalala.workers.dev/surge?token=xbox123';
|
const REMOTE_READ_URL = "https://xbox-bot.biubiubiu-lalala.workers.dev/surge?token=xbox123";
|
||||||
const REMOTE_COMMIT_URL = 'https://xbox-bot.biubiubiu-lalala.workers.dev/surge/commit?token=xbox123';
|
const REMOTE_COMMIT_URL = "https://xbox-bot.biubiubiu-lalala.workers.dev/surge/commit?token=xbox123";
|
||||||
const CLIENT_CONTEXT = { client: "UniversalWebStore.Cart", deviceType: "Pc" };
|
const CLIENT_CONTEXT = { client: "UniversalWebStore.Cart", deviceType: "Pc" };
|
||||||
const API_URL = "https://cart.production.store-web.dynamics.com/cart/v1.0/cart/loadCart?cartType=consumer&appId=StoreWeb";
|
const API_URL = "https://cart.production.store-web.dynamics.com/cart/v1.0/cart/loadCart?cartType=consumer&appId=StoreWeb";
|
||||||
|
|
||||||
// ========================= 解析 region 参数 =========================
|
|
||||||
function getRegionParam() {
|
function getRegionParam() {
|
||||||
try {
|
try {
|
||||||
const url = $request.url || "";
|
const url = $request.url || "";
|
||||||
@@ -77,7 +64,6 @@ function getRegionParam() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================= 区域选择页面 =========================
|
|
||||||
function serveSelector() {
|
function serveSelector() {
|
||||||
const html = `<!DOCTYPE html>
|
const html = `<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
@@ -87,60 +73,27 @@ function serveSelector() {
|
|||||||
<title>Xbox · 区域加购</title>
|
<title>Xbox · 区域加购</title>
|
||||||
<style>
|
<style>
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@400;600;700&family=Noto+Sans+SC:wght@400;500&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@400;600;700&family=Noto+Sans+SC:wght@400;500&display=swap');
|
||||||
|
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg: #0b0b0b;
|
--bg:#0b0b0b; --surface:#141414; --border:#222; --text:#ddd; --muted:#555;
|
||||||
--surface: #141414;
|
--green:#107c10; --green-hi:#52b043;
|
||||||
--border: #222;
|
|
||||||
--text: #ddd;
|
|
||||||
--muted: #555;
|
|
||||||
--green: #107c10;
|
|
||||||
--green-hi: #52b043;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%; background: var(--bg); color: var(--text);
|
||||||
background: var(--bg);
|
font-family: 'Noto Sans SC', sans-serif; -webkit-font-smoothing: antialiased;
|
||||||
color: var(--text);
|
|
||||||
font-family: 'Noto Sans SC', sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
display: flex;
|
display:flex; flex-direction:column; align-items:center; justify-content:center;
|
||||||
flex-direction: column;
|
min-height:100vh; padding:32px 20px;
|
||||||
align-items: center;
|
background-image: radial-gradient(ellipse 80% 50% at 50% -10%, rgba(16,124,16,0.12) 0%, transparent 60%);
|
||||||
justify-content: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 32px 20px;
|
|
||||||
background-image:
|
|
||||||
radial-gradient(ellipse 80% 50% at 50% -10%, rgba(16,124,16,0.12) 0%, transparent 60%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Xbox logo bar */
|
|
||||||
.xbox-bar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
}
|
||||||
|
.xbox-bar { display:flex; align-items:center; gap:10px; margin-bottom:32px; }
|
||||||
.xbox-sphere {
|
.xbox-sphere {
|
||||||
width:36px; height:36px;
|
width:36px; height:36px;
|
||||||
background: radial-gradient(circle at 38% 38%, #4caf50, #107c10 60%, #0a5a0a);
|
background: radial-gradient(circle at 38% 38%, #4caf50, #107c10 60%, #0a5a0a);
|
||||||
border-radius: 50%;
|
border-radius:50%; box-shadow:0 0 18px rgba(82,176,67,0.35); position:relative; flex-shrink:0;
|
||||||
box-shadow: 0 0 18px rgba(82,176,67,0.35);
|
|
||||||
position: relative;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.xbox-sphere::before,
|
|
||||||
.xbox-sphere::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
}
|
||||||
|
.xbox-sphere::before, .xbox-sphere::after { content:''; position:absolute; inset:0; border-radius:50%; }
|
||||||
.xbox-sphere::before {
|
.xbox-sphere::before {
|
||||||
border-top:1.5px solid rgba(255,255,255,0.25);
|
border-top:1.5px solid rgba(255,255,255,0.25);
|
||||||
border-left:1.5px solid rgba(255,255,255,0.1);
|
border-left:1.5px solid rgba(255,255,255,0.1);
|
||||||
@@ -149,133 +102,55 @@ function serveSelector() {
|
|||||||
transform:rotate(-20deg);
|
transform:rotate(-20deg);
|
||||||
}
|
}
|
||||||
.xbox-label {
|
.xbox-label {
|
||||||
font-family: 'Barlow Condensed', sans-serif;
|
font-family:'Barlow Condensed', sans-serif; font-size:22px; font-weight:700;
|
||||||
font-size: 22px;
|
letter-spacing:3px; text-transform:uppercase; color:#fff;
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 3px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
.xbox-label span { color: var(--green-hi); }
|
.xbox-label span { color: var(--green-hi); }
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-family: 'Barlow Condensed', sans-serif;
|
font-family:'Barlow Condensed', sans-serif; font-size:clamp(22px,5.5vw,32px);
|
||||||
font-size: clamp(22px, 5.5vw, 32px);
|
font-weight:600; letter-spacing:1px; color:#fff; text-align:center; margin-bottom:6px;
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
}
|
||||||
|
.hint { font-size:12px; color:var(--muted); margin-bottom:36px; text-align:center; letter-spacing:.5px; }
|
||||||
.hint {
|
.cards { display:flex; flex-direction:column; gap:12px; width:100%; max-width:420px; }
|
||||||
font-size: 12px;
|
|
||||||
color: var(--muted);
|
|
||||||
margin-bottom: 36px;
|
|
||||||
text-align: center;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Cards */
|
|
||||||
.cards {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 420px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
display: flex;
|
display:flex; align-items:center; gap:16px; padding:18px 20px;
|
||||||
align-items: center;
|
background:var(--surface); border:1px solid var(--border); border-radius:4px;
|
||||||
gap: 16px;
|
cursor:pointer; text-decoration:none; color:var(--text); position:relative; overflow:hidden;
|
||||||
padding: 18px 20px;
|
transition:border-color .2s, background .2s, transform .15s; -webkit-tap-highlight-color:transparent;
|
||||||
background: var(--surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--text);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: border-color .2s, background .2s, transform .15s;
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card::after {
|
.card::after {
|
||||||
content: '';
|
content:''; position:absolute; left:0; top:0; bottom:0; width:3px;
|
||||||
position: absolute;
|
background:var(--c); opacity:0; transition:opacity .2s;
|
||||||
left: 0; top: 0; bottom: 0;
|
|
||||||
width: 3px;
|
|
||||||
background: var(--c);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity .2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover, .card:active {
|
|
||||||
border-color: var(--c);
|
|
||||||
background: #1b1b1b;
|
|
||||||
transform: translateX(4px);
|
|
||||||
}
|
}
|
||||||
|
.card:hover, .card:active { border-color:var(--c); background:#1b1b1b; transform:translateX(4px); }
|
||||||
.card:hover::after, .card:active::after { opacity:1; }
|
.card:hover::after, .card:active::after { opacity:1; }
|
||||||
|
|
||||||
.card-flag { font-size:30px; line-height:1; flex-shrink:0; }
|
.card-flag { font-size:30px; line-height:1; flex-shrink:0; }
|
||||||
.card-info { flex:1; }
|
.card-info { flex:1; }
|
||||||
.card-name {
|
.card-name {
|
||||||
font-family: 'Barlow Condensed', sans-serif;
|
font-family:'Barlow Condensed', sans-serif; font-size:20px; font-weight:700;
|
||||||
font-size: 20px;
|
letter-spacing:1px; color:#fff;
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.card-meta { font-size: 11px; color: var(--muted); margin-top: 2px; letter-spacing: 0.8px; }
|
|
||||||
.card-arrow {
|
|
||||||
font-size: 20px;
|
|
||||||
color: var(--border);
|
|
||||||
transition: color .2s, transform .2s;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
|
.card-meta { font-size:11px; color:var(--muted); margin-top:2px; letter-spacing:.8px; }
|
||||||
|
.card-arrow { font-size:20px; color:var(--border); transition:color .2s, transform .2s; flex-shrink:0; }
|
||||||
.card:hover .card-arrow { color:var(--c); transform:translateX(3px); }
|
.card:hover .card-arrow { color:var(--c); transform:translateX(3px); }
|
||||||
|
|
||||||
/* Loading */
|
|
||||||
.overlay {
|
.overlay {
|
||||||
display: none;
|
display:none; position:fixed; inset:0; background:rgba(0,0,0,0.88); z-index:999;
|
||||||
position: fixed; inset: 0;
|
flex-direction:column; align-items:center; justify-content:center; gap:18px;
|
||||||
background: rgba(0,0,0,0.88);
|
|
||||||
z-index: 999;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 18px;
|
|
||||||
}
|
}
|
||||||
.overlay.on { display:flex; }
|
.overlay.on { display:flex; }
|
||||||
.spin {
|
.spin {
|
||||||
width: 44px; height: 44px;
|
width:44px; height:44px; border:3px solid #222; border-top-color:var(--green-hi);
|
||||||
border: 3px solid #222;
|
border-radius:50%; animation:spin .75s linear infinite;
|
||||||
border-top-color: var(--green-hi);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin .75s linear infinite;
|
|
||||||
}
|
}
|
||||||
.spin-label {
|
.spin-label {
|
||||||
font-family: 'Barlow Condensed', sans-serif;
|
font-family:'Barlow Condensed', sans-serif; font-size:15px; letter-spacing:2px;
|
||||||
font-size: 15px;
|
color:var(--muted); text-transform:uppercase;
|
||||||
letter-spacing: 2px;
|
|
||||||
color: var(--muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
@keyframes spin { to { transform: rotate(360deg); } }
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
footer { margin-top:44px; font-size:11px; color:#2a2a2a; letter-spacing:2px; text-transform:uppercase; }
|
||||||
footer {
|
|
||||||
margin-top: 44px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: #2a2a2a;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div class="xbox-bar">
|
<div class="xbox-bar">
|
||||||
<div class="xbox-sphere"></div>
|
<div class="xbox-sphere"></div>
|
||||||
<div class="xbox-label">X<span>box</span></div>
|
<div class="xbox-label">X<span>box</span></div>
|
||||||
@@ -340,7 +215,6 @@ function go(el, name) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================= 加购逻辑 =========================
|
|
||||||
function runCart(regionCode) {
|
function runCart(regionCode) {
|
||||||
const cfg = REGION_CONFIGS[regionCode];
|
const cfg = REGION_CONFIGS[regionCode];
|
||||||
if (!cfg) {
|
if (!cfg) {
|
||||||
@@ -353,7 +227,6 @@ function runCart(regionCode) {
|
|||||||
const MUID = $persistentStore.read(MUID_KEY);
|
const MUID = $persistentStore.read(MUID_KEY);
|
||||||
const MS_CV = $persistentStore.read(CV_KEY);
|
const MS_CV = $persistentStore.read(CV_KEY);
|
||||||
|
|
||||||
|
|
||||||
const HEADERS = {
|
const HEADERS = {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
"accept": "*/*",
|
"accept": "*/*",
|
||||||
@@ -382,7 +255,11 @@ function runCart(regionCode) {
|
|||||||
"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c =>
|
"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c =>
|
||||||
(c === "x" ? (Math.random() * 16 | 0) : ((Math.random() * 4 | 8) | 0)).toString(16));
|
(c === "x" ? (Math.random() * 16 | 0) : ((Math.random() * 4 | 8) | 0)).toString(16));
|
||||||
|
|
||||||
const toNum = k => { const m = /^product(\d+)$/.exec(k); return m ? parseInt(m[1], 10) : Number.MAX_SAFE_INTEGER; };
|
const toNum = k => {
|
||||||
|
const m = /^product(\d+)$/.exec(k);
|
||||||
|
return m ? parseInt(m[1], 10) : Number.MAX_SAFE_INTEGER;
|
||||||
|
};
|
||||||
|
|
||||||
const normEntry = v => {
|
const normEntry = v => {
|
||||||
if (!v || typeof v !== "object") return null;
|
if (!v || typeof v !== "object") return null;
|
||||||
const productId = String(v.ProductId ?? v.productId ?? "").trim();
|
const productId = String(v.ProductId ?? v.productId ?? "").trim();
|
||||||
@@ -393,19 +270,152 @@ function runCart(regionCode) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function parseProductList(raw) {
|
function parseProductList(raw) {
|
||||||
let parsed; try { parsed = JSON.parse(raw || "{}"); } catch { parsed = {}; }
|
let parsed;
|
||||||
|
try { parsed = JSON.parse(raw || "{}"); } catch { parsed = {}; }
|
||||||
return Object.keys(parsed)
|
return Object.keys(parsed)
|
||||||
.sort((a, b) => toNum(a) - toNum(b))
|
.sort((a, b) => toNum(a) - toNum(b))
|
||||||
.map(k => { const n = normEntry(parsed[k]); return n ? { key: k, ...n } : null; })
|
.map(k => {
|
||||||
|
const n = normEntry(parsed[k]);
|
||||||
|
return n ? { key: k, ...n } : null;
|
||||||
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normId = v => String(v ?? "").trim().toUpperCase();
|
||||||
|
const asArr = v => Array.isArray(v) ? v : [];
|
||||||
|
|
||||||
|
function parseJsonBody(raw) {
|
||||||
|
if (raw && typeof raw === "object") return { ok: true, value: raw };
|
||||||
|
try {
|
||||||
|
return { ok: true, value: JSON.parse(String(raw || "{}")) };
|
||||||
|
} catch (e) {
|
||||||
|
return { ok: false, error: String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusCode(response) {
|
||||||
|
return Number(response && (response.status || response.statusCode)) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectLineItems(cart) {
|
||||||
|
const out = [];
|
||||||
|
const addItems = items => {
|
||||||
|
for (const item of asArr(items)) {
|
||||||
|
if (item && typeof item === "object") out.push(item);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addItems(cart && cart.lineItems);
|
||||||
|
addItems(cart && cart.bundleLineItems);
|
||||||
|
|
||||||
|
for (const bundle of asArr(cart && cart.bundleLineItems)) {
|
||||||
|
addItems(bundle && bundle.lineItems);
|
||||||
|
addItems(bundle && bundle.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectBusinessErrors(payload) {
|
||||||
|
const events = payload && payload.events;
|
||||||
|
if (!events || typeof events !== "object") return [];
|
||||||
|
|
||||||
|
const found = [];
|
||||||
|
const visit = (section, value) => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const event of value) visit(section, event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!value || typeof value !== "object") return;
|
||||||
|
|
||||||
|
const data = value.data || {};
|
||||||
|
const status = Number(data.httpStatusCode || value.httpStatusCode || 0);
|
||||||
|
const type = String(value.type || value.severity || "").toLowerCase();
|
||||||
|
|
||||||
|
if (type === "error" || status >= 400) {
|
||||||
|
found.push({ section, event: value });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(value)) {
|
||||||
|
if (Array.isArray(value[key])) visit(`${section}.${key}`, value[key]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const key of Object.keys(events)) visit(key, events[key]);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBusinessError(item) {
|
||||||
|
const event = item.event || {};
|
||||||
|
const data = event.data || {};
|
||||||
|
const parts = [];
|
||||||
|
|
||||||
|
if (item.section) parts.push(item.section);
|
||||||
|
if (event.provider) parts.push(`provider=${event.provider}`);
|
||||||
|
if (event.code) parts.push(`code=${event.code}`);
|
||||||
|
if (data.reason) parts.push(`reason=${data.reason}`);
|
||||||
|
if (Array.isArray(data.subReasons) && data.subReasons.length) {
|
||||||
|
parts.push(`sub=${data.subReasons.filter(Boolean).join(" / ")}`);
|
||||||
|
}
|
||||||
|
if (data.httpStatusCode) parts.push(`http=${data.httpStatusCode}`);
|
||||||
|
|
||||||
|
return parts.join(", ") || "unknown business error";
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateAddResult(rawBody, target) {
|
||||||
|
const parsed = parseJsonBody(rawBody);
|
||||||
|
if (!parsed.ok) {
|
||||||
|
return { ok: false, reason: `响应不是合法 JSON: ${parsed.error}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = parsed.value || {};
|
||||||
|
const cart = payload.cart || {};
|
||||||
|
const market = cart.market ? String(cart.market).toUpperCase() : "";
|
||||||
|
const language = cart.language ? String(cart.language).toLowerCase() : "";
|
||||||
|
const contextProblems = [];
|
||||||
|
|
||||||
|
if (market && market !== MARKET) contextProblems.push(`market=${cart.market}`);
|
||||||
|
if (language && language !== LOCALE.toLowerCase()) contextProblems.push(`language=${cart.language}`);
|
||||||
|
|
||||||
|
const errors = collectBusinessErrors(payload);
|
||||||
|
const lineItems = collectLineItems(cart);
|
||||||
|
const matchedLine = lineItems.find(item =>
|
||||||
|
normId(item.productId) === normId(target.productId) &&
|
||||||
|
normId(item.skuId) === normId(target.skuId) &&
|
||||||
|
normId(item.availabilityId) === normId(target.availabilityId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
const detail = errors.map(formatBusinessError).join(" | ");
|
||||||
|
const context = contextProblems.length ? ` | context: ${contextProblems.join(", ")}` : "";
|
||||||
|
return { ok: false, reason: `${detail}${context} | lineItems=${lineItems.length}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contextProblems.length > 0) {
|
||||||
|
return { ok: false, reason: `购物车上下文不匹配: ${contextProblems.join(", ")} | lineItems=${lineItems.length}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchedLine) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: `HTTP 200 但未在 cart.lineItems 找到目标商品 | lineItems=${lineItems.length} | cartId=${cart.id || ""}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = matchedLine.title ? ` | ${matchedLine.title}` : "";
|
||||||
|
const qty = matchedLine.quantity ? ` | qty=${matchedLine.quantity}` : "";
|
||||||
|
const amount = matchedLine.totalAmount != null ? ` | ${CURRENCY} ${Number(matchedLine.totalAmount).toFixed(2)}` : "";
|
||||||
|
return { ok: true, detail: `${cart.language || ""}/${cart.market || ""}${title}${qty}${amount}` };
|
||||||
|
}
|
||||||
|
|
||||||
function buildResultPage(failedNames) {
|
function buildResultPage(failedNames) {
|
||||||
const sc = results.success.length;
|
const sc = results.success.length;
|
||||||
const fc = results.failure.length;
|
const fc = results.failure.length;
|
||||||
const failedHtml = failedNames.length
|
const failedHtml = failedNames.length
|
||||||
? `<div class="failed-box"><b>加购失败的游戏:</b><ul>${failedNames.map(n => `<li>${n}</li>`).join("")}</ul></div>`
|
? `<div class="failed-box"><b>加购失败的游戏:</b><ul>${failedNames.map(n => `<li>${n}</li>`).join("")}</ul></div>`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
@@ -460,7 +470,11 @@ ${failedHtml}
|
|||||||
$done({
|
$done({
|
||||||
response: {
|
response: {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { "Content-Type": "text/html;charset=utf-8", "Cache-Control": "no-store, no-cache, must-revalidate", "Pragma": "no-cache" },
|
headers: {
|
||||||
|
"Content-Type": "text/html;charset=utf-8",
|
||||||
|
"Cache-Control": "no-store, no-cache, must-revalidate",
|
||||||
|
"Pragma": "no-cache"
|
||||||
|
},
|
||||||
body: buildResultPage(failedNames)
|
body: buildResultPage(failedNames)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -471,10 +485,16 @@ ${failedHtml}
|
|||||||
let fi = 1;
|
let fi = 1;
|
||||||
for (const item of productList) {
|
for (const item of productList) {
|
||||||
if (results.failure.includes(item.productId)) {
|
if (results.failure.includes(item.productId)) {
|
||||||
failedProducts[`product${fi++}`] = { ProductId: item.productId, SkuId: item.skuId, AvailabilityId: item.availabilityId };
|
failedProducts[`product${fi++}`] = {
|
||||||
|
ProductId: item.productId,
|
||||||
|
SkuId: item.skuId,
|
||||||
|
AvailabilityId: item.availabilityId
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log("info", fc === 0 ? "全部成功,提交 commit(弹出当前组)" : `${fc} 个失败,提交 commit(保留失败部分)`);
|
log("info", fc === 0 ? "全部成功,提交 commit(弹出当前组)" : `${fc} 个失败,提交 commit(保留失败部分)`);
|
||||||
|
|
||||||
$httpClient.post({
|
$httpClient.post({
|
||||||
url: REMOTE_COMMIT_URL,
|
url: REMOTE_COMMIT_URL,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -486,48 +506,79 @@ ${failedHtml}
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
let store; try { store = JSON.parse($persistentStore.read(LOCAL_KEY) || "{}"); } catch { store = {}; }
|
let store;
|
||||||
|
try { store = JSON.parse($persistentStore.read(LOCAL_KEY) || "{}"); } catch { store = {}; }
|
||||||
|
|
||||||
for (const k of successKeys) {
|
for (const k of successKeys) {
|
||||||
if (k && Object.prototype.hasOwnProperty.call(store, k)) delete store[k];
|
if (k && Object.prototype.hasOwnProperty.call(store, k)) delete store[k];
|
||||||
}
|
}
|
||||||
|
|
||||||
const rem = Object.keys(store).filter(k => normEntry(store[k]) !== null).length;
|
const rem = Object.keys(store).filter(k => normEntry(store[k]) !== null).length;
|
||||||
$persistentStore.write(JSON.stringify(store), LOCAL_KEY);
|
$persistentStore.write(JSON.stringify(store), LOCAL_KEY);
|
||||||
log("info", "本地清理完成", `剩余: ${rem}`);
|
log("info", "本地清理完成", `剩余: ${rem}`);
|
||||||
} catch (e) { log("error", "清理异常", String(e)); }
|
} catch (e) {
|
||||||
|
log("error", "清理异常", String(e));
|
||||||
|
}
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendRequest() {
|
function sendRequest() {
|
||||||
if (currentIndex >= productList.length) return finalizeAndClean();
|
if (currentIndex >= productList.length) return finalizeAndClean();
|
||||||
|
|
||||||
const { key, productId, skuId, availabilityId } = productList[currentIndex];
|
const { key, productId, skuId, availabilityId } = productList[currentIndex];
|
||||||
|
|
||||||
$httpClient.put({
|
$httpClient.put({
|
||||||
url: API_URL,
|
url: API_URL,
|
||||||
headers: HEADERS,
|
headers: HEADERS,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
locale: LOCALE, market: MARKET,
|
locale: LOCALE,
|
||||||
|
market: MARKET,
|
||||||
catalogClientType: "storeWeb",
|
catalogClientType: "storeWeb",
|
||||||
friendlyName: FRIENDLY_NAME,
|
friendlyName: FRIENDLY_NAME,
|
||||||
riskSessionId: riskId(),
|
riskSessionId: riskId(),
|
||||||
clientContext: CLIENT_CONTEXT,
|
clientContext: CLIENT_CONTEXT,
|
||||||
itemsToAdd: { items: [{ productId, skuId, availabilityId, campaignId: "xboxcomct", quantity: 1 }] }
|
itemsToAdd: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
productId,
|
||||||
|
skuId,
|
||||||
|
availabilityId,
|
||||||
|
campaignId: "xboxcomct",
|
||||||
|
quantity: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}, (error, response) => {
|
}, (error, response, data) => {
|
||||||
if (error || response.status !== 200) {
|
const statusCode = getStatusCode(response);
|
||||||
|
|
||||||
|
if (error || statusCode !== 200) {
|
||||||
results.failure.push(productId);
|
results.failure.push(productId);
|
||||||
log("error", "失败", productId);
|
log("error", "失败", `${productId} | HTTP ${statusCode || "ERR"} ${error ? String(error) : ""}`);
|
||||||
} else {
|
} else {
|
||||||
|
const verdict = validateAddResult(
|
||||||
|
data != null ? data : (response && response.body),
|
||||||
|
{ productId, skuId, availabilityId }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (verdict.ok) {
|
||||||
results.success.push(productId);
|
results.success.push(productId);
|
||||||
if (key) successKeys.push(key);
|
if (key) successKeys.push(key);
|
||||||
log("success", "成功", productId);
|
log("success", "成功", `${productId}${verdict.detail ? " | " + verdict.detail : ""}`);
|
||||||
|
} else {
|
||||||
|
results.failure.push(productId);
|
||||||
|
log("error", "失败", `${productId} | ${verdict.reason}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
currentIndex++;
|
currentIndex++;
|
||||||
setTimeout(sendRequest, 50);
|
setTimeout(sendRequest, 50);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function doneWithPage(title, message, type = "warn") {
|
function doneWithPage(title, message, type = "warn") {
|
||||||
const color = type === "error" ? "#e05050" : type === "warn" ? "#e8a838" : "#52b043";
|
const pageColor = type === "error" ? "#e05050" : type === "warn" ? "#e8a838" : "#52b043";
|
||||||
const icon = type === "error" ? "❌" : type === "warn" ? "⚠️" : "✅";
|
const icon = type === "error" ? "❌" : type === "warn" ? "⚠️" : "✅";
|
||||||
const html = `<!DOCTYPE html>
|
const html = `<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
@@ -539,13 +590,13 @@ ${failedHtml}
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@600;700&family=Noto+Sans+SC:wght@400;500&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@600;700&family=Noto+Sans+SC:wght@400;500&display=swap');
|
||||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
body{background:#0b0b0b;color:#ddd;font-family:'Noto Sans SC',sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px 18px;-webkit-font-smoothing:antialiased}
|
body{background:#0b0b0b;color:#ddd;font-family:'Noto Sans SC',sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px 18px;-webkit-font-smoothing:antialiased}
|
||||||
.card{width:100%;max-width:420px;background:#141414;border:1px solid #222;border-top:3px solid ${color};border-radius:4px;padding:24px 20px 20px}
|
.card{width:100%;max-width:420px;background:#141414;border:1px solid #222;border-top:3px solid ${pageColor};border-radius:4px;padding:24px 20px 20px}
|
||||||
.icon{font-size:32px;margin-bottom:12px}
|
.icon{font-size:32px;margin-bottom:12px}
|
||||||
.title{font-family:'Barlow Condensed',sans-serif;font-size:20px;font-weight:700;color:#fff;letter-spacing:1px;margin-bottom:8px}
|
.title{font-family:'Barlow Condensed',sans-serif;font-size:20px;font-weight:700;color:#fff;letter-spacing:1px;margin-bottom:8px}
|
||||||
.msg{font-size:13px;color:#888;line-height:1.7}
|
.msg{font-size:13px;color:#888;line-height:1.7}
|
||||||
.footer{margin-top:20px;display:flex;align-items:center;justify-content:space-between}
|
.footer{margin-top:20px;display:flex;align-items:center;justify-content:space-between}
|
||||||
.sub{font-size:11px;color:#2a2a2a;letter-spacing:1.5px;text-transform:uppercase}
|
.sub{font-size:11px;color:#2a2a2a;letter-spacing:1.5px;text-transform:uppercase}
|
||||||
a{display:inline-block;padding:8px 18px;border:1px solid ${color};color:${color};border-radius:3px;font-family:'Barlow Condensed',sans-serif;font-size:13px;font-weight:600;letter-spacing:1px;text-decoration:none}
|
a{display:inline-block;padding:8px 18px;border:1px solid ${pageColor};color:${pageColor};border-radius:3px;font-family:'Barlow Condensed',sans-serif;font-size:13px;font-weight:600;letter-spacing:1px;text-decoration:none}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -560,19 +611,35 @@ ${failedHtml}
|
|||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
$done({ response: { status: 200, headers: { "Content-Type": "text/html;charset=utf-8", "Cache-Control": "no-store, no-cache, must-revalidate", "Pragma": "no-cache" }, body: html } });
|
|
||||||
|
$done({
|
||||||
|
response: {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/html;charset=utf-8",
|
||||||
|
"Cache-Control": "no-store, no-cache, must-revalidate",
|
||||||
|
"Pragma": "no-cache"
|
||||||
|
},
|
||||||
|
body: html
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function startTask() {
|
function startTask() {
|
||||||
if (!MUID || !MS_CV) {
|
if (!MUID || !MS_CV) {
|
||||||
$notification.post(`❌ Xbox ${label} 错误`, `缺少 MUID 或 CV`, `请写入 ${MUID_KEY} / ${CV_KEY}`);
|
$notification.post(`❌ Xbox ${label} 错误`, "缺少 MUID 或 CV", `请写入 ${MUID_KEY} / ${CV_KEY}`);
|
||||||
doneWithPage("缺少必要参数", `未找到 MUID 或 MS-CV,请确认已正确写入:<br><br><code>${MUID_KEY}</code><br><code>${CV_KEY}</code>`, "error");
|
doneWithPage("缺少必要参数", `未找到 MUID 或 MS-CV,请确认已正确写入:<br><br><code>${MUID_KEY}</code><br><code>${CV_KEY}</code>`, "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (productList.length === 0) {
|
if (productList.length === 0) {
|
||||||
$notification.post(`⚠️ Xbox ${label}`, "列表为空,无需执行", `来源: ${sourceLabel}`);
|
$notification.post(`⚠️ Xbox ${label}`, "列表为空,无需执行", `来源: ${sourceLabel}`);
|
||||||
if (useRemote) {
|
if (useRemote) {
|
||||||
$httpClient.post({ url: REMOTE_COMMIT_URL, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ remaining: {} }) }, () => {
|
$httpClient.post({
|
||||||
|
url: REMOTE_COMMIT_URL,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ remaining: {} })
|
||||||
|
}, () => {
|
||||||
doneWithPage("暂无商品", `远程队列与本地列表均为空,无需加购。<br><br>来源: ${sourceLabel}`, "warn");
|
doneWithPage("暂无商品", `远程队列与本地列表均为空,无需加购。<br><br>来源: ${sourceLabel}`, "warn");
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -580,13 +647,15 @@ ${failedHtml}
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log("info", `开始 ${flag}${label} 加购`, `数量: ${productList.length},来源: ${sourceLabel}`);
|
log("info", `开始 ${flag}${label} 加购`, `数量: ${productList.length},来源: ${sourceLabel}`);
|
||||||
sendRequest();
|
sendRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 主流程
|
|
||||||
$httpClient.get(REMOTE_READ_URL, (err, _res, data) => {
|
$httpClient.get(REMOTE_READ_URL, (err, _res, data) => {
|
||||||
let remoteGroup = null, groupIndex = null;
|
let remoteGroup = null;
|
||||||
|
let groupIndex = null;
|
||||||
|
|
||||||
if (!err && data) {
|
if (!err && data) {
|
||||||
try {
|
try {
|
||||||
const p = JSON.parse((data || "").trim() || "{}");
|
const p = JSON.parse((data || "").trim() || "{}");
|
||||||
@@ -596,10 +665,11 @@ ${failedHtml}
|
|||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remoteGroup) {
|
if (remoteGroup) {
|
||||||
useRemote = true;
|
useRemote = true;
|
||||||
sourceLabel = `远程第 ${groupIndex} 组`;
|
sourceLabel = `远程第 ${groupIndex} 组`;
|
||||||
log("info", `使用远程 Product`, `${flag}${label} · 第 ${groupIndex} 组,共 ${Object.keys(remoteGroup).length} 个`);
|
log("info", "使用远程 Product", `${flag}${label} · 第 ${groupIndex} 组,共 ${Object.keys(remoteGroup).length} 个`);
|
||||||
productList = parseProductList(JSON.stringify(remoteGroup));
|
productList = parseProductList(JSON.stringify(remoteGroup));
|
||||||
} else {
|
} else {
|
||||||
useRemote = false;
|
useRemote = false;
|
||||||
@@ -607,12 +677,13 @@ ${failedHtml}
|
|||||||
productList = parseProductList($persistentStore.read(LOCAL_KEY) || "{}");
|
productList = parseProductList($persistentStore.read(LOCAL_KEY) || "{}");
|
||||||
log("info", err ? `远程连接失败,使用本地 [${label}]` : `远程队列为空,使用本地 [${label}]`, err ? String(err) : "");
|
log("info", err ? `远程连接失败,使用本地 [${label}]` : `远程队列为空,使用本地 [${label}]`, err ? String(err) : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
startTask();
|
startTask();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================= 入口 =========================
|
|
||||||
const region = getRegionParam();
|
const region = getRegionParam();
|
||||||
|
|
||||||
if (!region) {
|
if (!region) {
|
||||||
serveSelector();
|
serveSelector();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user