diff --git a/Scripts/AddMsGames.js b/Scripts/AddMsGames.js
index a4ae61c..759b010 100644
--- a/Scripts/AddMsGames.js
+++ b/Scripts/AddMsGames.js
@@ -9,65 +9,52 @@
* - 访问 https://addmsgames.com/?region=US → 直接执行美区加购
* - 访问 https://addmsgames.com/?region=NG → 直接执行尼区加购
* - 访问 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 = {
US: {
- label: "美区",
- flag: "🇺🇸",
- color: "#4a90e2",
- MARKET: "US",
- LOCALE: "en-us",
- FRIENDLY_NAME:"cart-US",
- MUID_KEY: "cart-x-authorization-muid",
- CV_KEY: "cart-ms-cv",
- LOCAL_KEY: "XboxProductList",
- CURRENCY: "USD",
+ label: "美区",
+ flag: "🇺🇸",
+ color: "#4a90e2",
+ MARKET: "US",
+ LOCALE: "en-us",
+ FRIENDLY_NAME: "cart-US",
+ MUID_KEY: "cart-x-authorization-muid",
+ CV_KEY: "cart-ms-cv",
+ LOCAL_KEY: "XboxProductList",
+ CURRENCY: "USD",
},
NG: {
- label: "尼区",
- flag: "🇳🇬",
- color: "#52b043",
- MARKET: "NG",
- LOCALE: "en-ng",
- FRIENDLY_NAME:"cart-NG",
- MUID_KEY: "cart-x-authorization-muid",
- CV_KEY: "cart-ms-cv",
- LOCAL_KEY: "XboxProductList",
- CURRENCY: "NGN",
+ label: "尼区",
+ flag: "🇳🇬",
+ color: "#52b043",
+ MARKET: "NG",
+ LOCALE: "en-ng",
+ FRIENDLY_NAME: "cart-NG",
+ MUID_KEY: "cart-x-authorization-muid",
+ CV_KEY: "cart-ms-cv",
+ LOCAL_KEY: "XboxProductList",
+ CURRENCY: "NGN",
},
AR: {
- label: "阿区",
- flag: "🇦🇷",
- color: "#e8a838",
- MARKET: "AR",
- LOCALE: "es-ar",
- FRIENDLY_NAME:"cart-AR",
- MUID_KEY: "cart-x-authorization-muid",
- CV_KEY: "cart-ms-cv",
- LOCAL_KEY: "XboxProductList",
- CURRENCY: "ARS",
+ label: "阿区",
+ flag: "🇦🇷",
+ color: "#e8a838",
+ MARKET: "AR",
+ LOCALE: "es-ar",
+ FRIENDLY_NAME: "cart-AR",
+ MUID_KEY: "cart-x-authorization-muid",
+ CV_KEY: "cart-ms-cv",
+ LOCAL_KEY: "XboxProductList",
+ CURRENCY: "ARS",
},
};
-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 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 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 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";
-// ========================= 解析 region 参数 =========================
function getRegionParam() {
try {
const url = $request.url || "";
@@ -77,7 +64,6 @@ function getRegionParam() {
return null;
}
-// ========================= 区域选择页面 =========================
function serveSelector() {
const html = `
@@ -87,195 +73,84 @@ function serveSelector() {
Xbox
@@ -340,7 +215,6 @@ function go(el, name) {
});
}
-// ========================= 加购逻辑 =========================
function runCart(regionCode) {
const cfg = REGION_CONFIGS[regionCode];
if (!cfg) {
@@ -350,10 +224,9 @@ function runCart(regionCode) {
}
const { label, flag, color, MARKET, LOCALE, FRIENDLY_NAME, MUID_KEY, CV_KEY, LOCAL_KEY, CURRENCY } = cfg;
- const MUID = $persistentStore.read(MUID_KEY);
+ const MUID = $persistentStore.read(MUID_KEY);
const MS_CV = $persistentStore.read(CV_KEY);
-
const HEADERS = {
"content-type": "application/json",
"accept": "*/*",
@@ -363,17 +236,17 @@ function runCart(regionCode) {
"user-agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1"
};
- let logBuffer = [];
- const results = { success: [], failure: [] };
+ let logBuffer = [];
+ const results = { success: [], failure: [] };
const successKeys = [];
- let currentIndex = 0;
- let productList = [];
- let sourceLabel = "";
- let useRemote = false;
+ let currentIndex = 0;
+ let productList = [];
+ let sourceLabel = "";
+ let useRemote = false;
function log(type, message, detail = "") {
- const icon = type === "success" ? "✅" : (type === "error" ? "❌" : "ℹ️");
- const clr = type === "success" ? "#52b043" : (type === "error" ? "#e05050" : "#777");
+ const icon = type === "success" ? "✅" : (type === "error" ? "❌" : "ℹ️");
+ const clr = type === "success" ? "#52b043" : (type === "error" ? "#e05050" : "#777");
console.log(`${icon} [${regionCode}] ${message} ${detail}`);
logBuffer.push(`
${icon} ${message} ${detail}`);
}
@@ -382,30 +255,167 @@ function runCart(regionCode) {
"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c =>
(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 => {
if (!v || typeof v !== "object") return null;
- const productId = String(v.ProductId ?? v.productId ?? "").trim();
- const skuId = String(v.SkuId ?? v.skuId ?? "").trim();
+ const productId = String(v.ProductId ?? v.productId ?? "").trim();
+ const skuId = String(v.SkuId ?? v.skuId ?? "").trim();
const availabilityId = String(v.AvailabilityId ?? v.availabilityId ?? "").trim();
if (!productId || !skuId || !availabilityId) return null;
return { productId, skuId, availabilityId };
};
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)
.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);
}
+ 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) {
const sc = results.success.length;
const fc = results.failure.length;
const failedHtml = failedNames.length
? `
加购失败的游戏:${failedNames.map(n => `- ${n}
`).join("")}
`
: "";
+
return `
@@ -441,7 +451,7 @@ function runCart(regionCode) {
${failedHtml}
来源: ${sourceLabel}
@@ -460,7 +470,11 @@ ${failedHtml}
$done({
response: {
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)
}
});
@@ -471,10 +485,16 @@ ${failedHtml}
let fi = 1;
for (const item of productList) {
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(保留失败部分)`);
+
$httpClient.post({
url: REMOTE_COMMIT_URL,
headers: { "Content-Type": "application/json" },
@@ -486,49 +506,80 @@ ${failedHtml}
});
} else {
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) {
if (k && Object.prototype.hasOwnProperty.call(store, k)) delete store[k];
}
+
const rem = Object.keys(store).filter(k => normEntry(store[k]) !== null).length;
$persistentStore.write(JSON.stringify(store), LOCAL_KEY);
log("info", "本地清理完成", `剩余: ${rem}`);
- } catch (e) { log("error", "清理异常", String(e)); }
+ } catch (e) {
+ log("error", "清理异常", String(e));
+ }
finish();
}
}
function sendRequest() {
if (currentIndex >= productList.length) return finalizeAndClean();
+
const { key, productId, skuId, availabilityId } = productList[currentIndex];
+
$httpClient.put({
url: API_URL,
headers: HEADERS,
body: JSON.stringify({
- locale: LOCALE, market: MARKET,
+ locale: LOCALE,
+ market: MARKET,
catalogClientType: "storeWeb",
friendlyName: FRIENDLY_NAME,
riskSessionId: riskId(),
clientContext: CLIENT_CONTEXT,
- itemsToAdd: { items: [{ productId, skuId, availabilityId, campaignId: "xboxcomct", quantity: 1 }] }
+ itemsToAdd: {
+ items: [
+ {
+ productId,
+ skuId,
+ availabilityId,
+ campaignId: "xboxcomct",
+ quantity: 1
+ }
+ ]
+ }
})
- }, (error, response) => {
- if (error || response.status !== 200) {
+ }, (error, response, data) => {
+ const statusCode = getStatusCode(response);
+
+ if (error || statusCode !== 200) {
results.failure.push(productId);
- log("error", "失败", productId);
+ log("error", "失败", `${productId} | HTTP ${statusCode || "ERR"} ${error ? String(error) : ""}`);
} else {
- results.success.push(productId);
- if (key) successKeys.push(key);
- log("success", "成功", productId);
+ const verdict = validateAddResult(
+ data != null ? data : (response && response.body),
+ { productId, skuId, availabilityId }
+ );
+
+ if (verdict.ok) {
+ results.success.push(productId);
+ if (key) successKeys.push(key);
+ log("success", "成功", `${productId}${verdict.detail ? " | " + verdict.detail : ""}`);
+ } else {
+ results.failure.push(productId);
+ log("error", "失败", `${productId} | ${verdict.reason}`);
+ }
}
+
currentIndex++;
setTimeout(sendRequest, 50);
});
}
function doneWithPage(title, message, type = "warn") {
- const color = type === "error" ? "#e05050" : type === "warn" ? "#e8a838" : "#52b043";
- const icon = type === "error" ? "❌" : type === "warn" ? "⚠️" : "✅";
+ const pageColor = type === "error" ? "#e05050" : type === "warn" ? "#e8a838" : "#52b043";
+ const icon = type === "error" ? "❌" : type === "warn" ? "⚠️" : "✅";
const html = `
@@ -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');
*,*::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}
- .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}
.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}
.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}
- 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}
@@ -560,19 +611,35 @@ ${failedHtml}
`;
- $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() {
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,请确认已正确写入: