增强MAA作业站的筛选功能
目前為
// ==UserScript==
// @name MaaCopilotPlus
// @namespace https://github.com/HauKuen
// @version 2.2.0
// @author haukuen
// @description 增强MAA作业站的筛选功能
// @license MIT
// @icon https://zoot.plus/favicon-32x32.png?v=1
// @homepage https://github.com/haukuen/maa-copilot-plus
// @match https://prts.plus/*
// @match https://zoot.plus/*
// @grant GM_getValue
// @grant GM_setValue
// @grant unsafeWindow
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
var _GM_getValue = (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
var _GM_setValue = (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
var _unsafeWindow = (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)();
const COPILOT_QUERY_URL = "prts.maa.plus/copilot/query";
const pageWindow = _unsafeWindow || window;
const _originalFetch = pageWindow.fetch.bind(pageWindow);
const _originalXHROpen = pageWindow.XMLHttpRequest.prototype.open;
const _originalXHRSend = pageWindow.XMLHttpRequest.prototype.send;
pageWindow.fetch = async function(input, init) {
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
const response = await _originalFetch(input, init);
if (url.includes(COPILOT_QUERY_URL)) {
try {
const json = await response.clone().json();
return new Response(JSON.stringify(filterResponse(json)), {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
} catch (e) {
console.warn("拦截 fetch 失败:", e);
}
}
return response;
};
pageWindow.XMLHttpRequest.prototype.open = function(method, url, ...args) {
this._url = url.toString();
this._isCopilotQuery = this._url.includes(
COPILOT_QUERY_URL
);
return _originalXHROpen.apply(this, [method, url, ...args]);
};
pageWindow.XMLHttpRequest.prototype.send = function(body) {
if (this._isCopilotQuery) {
const xhr = this;
const originalOnReadyStateChange = xhr.onreadystatechange;
xhr.onreadystatechange = function(ev) {
if (xhr.readyState === 4 && xhr.status === 200) {
try {
const filtered = filterResponse(JSON.parse(xhr.responseText));
Object.defineProperty(xhr, "responseText", {
get: () => JSON.stringify(filtered)
});
Object.defineProperty(xhr, "response", {
get: () => JSON.stringify(filtered)
});
} catch (e) {
console.warn("拦截 XHR 失败:", e);
}
}
originalOnReadyStateChange?.call(xhr, ev);
};
}
return _originalXHRSend.call(this, body);
};
let myOperators = _GM_getValue("myOperators", []);
let filterEnabled = _GM_getValue("filterEnabled", true);
let allowOneMissing = _GM_getValue("allowOneMissing", false);
function checkOperator(oper) {
const myOp = myOperators.find((op) => op.name === oper.name);
if (!myOp) return false;
if (myOp.rarity === 6 && myOp.elite < 2) return false;
return (oper.skill || 1) <= myOp.maxSkill;
}
function checkGroup(group) {
if (!group.opers?.length) return true;
return group.opers.some((oper) => checkOperator(oper));
}
function checkCopilotItem(item) {
try {
const content = JSON.parse(item.content);
let missingCount = 0;
content.opers?.forEach((oper) => {
if (!checkOperator(oper)) missingCount++;
});
content.groups?.forEach((group) => {
if (!checkGroup(group)) missingCount++;
});
return {
pass: allowOneMissing ? missingCount <= 1 : missingCount === 0,
missingCount
};
} catch {
return { pass: true, missingCount: 0 };
}
}
function filterResponse(response) {
if (!filterEnabled || !myOperators.length) return response;
const originalData = response.data.data;
const filteredData = originalData.filter(
(item) => checkCopilotItem(item).pass
);
setTimeout(() => updateStatus(), 100);
return { ...response, data: { ...response.data, data: filteredData } };
}
function createNavButton(text, isActive, onClick, title) {
const btn = document.createElement("button");
btn.className = `bp4-button bp4-minimal ${isActive ? "bp4-active" : ""}`;
btn.type = "button";
const span = document.createElement("span");
span.className = "bp4-button-text";
span.textContent = text;
btn.appendChild(span);
btn.onclick = onClick;
if (title) btn.title = title;
return btn;
}
function injectToNavbar() {
const navbar = document.querySelector(".bp4-navbar");
if (!navbar) {
setTimeout(injectToNavbar, 1e3);
return;
}
const rightContainer = navbar.querySelector(".flex.md\\:gap-4");
if (!rightContainer) {
setTimeout(injectToNavbar, 1e3);
return;
}
if (document.getElementById("maa-copilot-plus")) return;
const container = document.createElement("div");
container.id = "maa-copilot-plus";
container.className = "flex items-center gap-2 mr-2";
const filterBtn = createNavButton(
filterEnabled ? "筛选中" : "筛选",
filterEnabled,
() => {
filterEnabled = !filterEnabled;
_GM_setValue("filterEnabled", filterEnabled);
filterBtn.classList.toggle("bp4-active", filterEnabled);
const textSpan = filterBtn.querySelector(".bp4-button-text");
if (textSpan) textSpan.textContent = filterEnabled ? "筛选中" : "筛选";
updateNavStatus();
if (confirm("筛选设置已更改,需要刷新页面才能生效。是否立即刷新?")) {
location.reload();
}
},
"开启/关闭自动筛选"
);
const missingBtn = createNavButton(
"允许缺1",
allowOneMissing,
() => {
allowOneMissing = !allowOneMissing;
_GM_setValue("allowOneMissing", allowOneMissing);
missingBtn.classList.toggle("bp4-active", allowOneMissing);
if (confirm("筛选设置已更改,需要刷新页面才能生效。是否立即刷新?")) {
location.reload();
}
},
"允许缺少一个干员"
);
const importBtn = createNavButton(
"导入角色",
false,
openImportDialog,
"导入角色列表"
);
const status = document.createElement("span");
status.id = "maa-status";
status.className = "text-sm text-zinc-600 dark:text-slate-100 ml-2 select-none";
container.append(filterBtn, missingBtn, importBtn, status);
rightContainer.insertBefore(container, rightContainer.firstChild);
updateNavStatus();
}
function updateNavStatus() {
const status = document.getElementById("maa-status");
if (!status) return;
let text = `${myOperators.length}个角色`;
status.textContent = text;
}
function updateStatus() {
updateNavStatus();
}
function openImportDialog() {
const modal = document.createElement("div");
Object.assign(modal.style, {
position: "fixed",
top: "0",
left: "0",
width: "100%",
height: "100%",
backgroundColor: "rgba(0,0,0,0.5)",
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: "5000"
});
const dialog = document.createElement("div");
dialog.className = "bp4-card bp4-elevation-3";
Object.assign(dialog.style, {
backgroundColor: "var(--bp4-load-app-background-color, #fff)",
minWidth: "400px",
padding: "20px",
borderRadius: "4px"
});
if (document.documentElement.classList.contains("dark")) {
dialog.style.backgroundColor = "#2f3946";
dialog.style.color = "#fff";
} else {
dialog.style.backgroundColor = "#fff";
dialog.style.color = "#000";
}
const title = document.createElement("h3");
title.className = "bp4-heading";
title.textContent = "导入角色列表";
title.style.marginTop = "0";
const textarea = document.createElement("textarea");
textarea.className = "bp4-input bp4-large";
Object.assign(textarea.style, {
width: "100%",
height: "200px",
marginBottom: "10px",
resize: "vertical"
});
textarea.placeholder = "粘贴角色列表 JSON 数据...";
textarea.value = JSON.stringify(myOperators, null, 2);
const buttonContainer = document.createElement("div");
buttonContainer.className = "flex justify-end gap-2";
Object.assign(buttonContainer.style, {
display: "flex",
justifyContent: "flex-end",
gap: "10px"
});
const cancelButton = document.createElement("button");
cancelButton.className = "bp4-button";
cancelButton.textContent = "取消";
cancelButton.onclick = () => document.body.removeChild(modal);
const confirmBtn = document.createElement("button");
confirmBtn.className = "bp4-button bp4-intent-primary";
confirmBtn.textContent = "导入";
confirmBtn.onclick = () => {
try {
const data = JSON.parse(textarea.value);
if (!Array.isArray(data)) {
alert("无效的数据格式");
return;
}
myOperators = data.filter((op) => op.own).map((op) => ({
name: op.name,
elite: op.elite,
level: op.level,
rarity: op.rarity,
maxSkill: op.elite === 0 ? 1 : op.elite === 1 ? 2 : 3
}));
_GM_setValue("myOperators", myOperators);
updateStatus();
document.body.removeChild(modal);
if (confirm("角色列表已导入,需要刷新页面才能生效。是否立即刷新?"))
location.reload();
} catch (e) {
alert("解析失败: " + e.message);
}
};
buttonContainer.append(cancelButton, confirmBtn);
dialog.append(title, textarea, buttonContainer);
modal.appendChild(dialog);
document.body.appendChild(modal);
}
function initUI() {
injectToNavbar();
const observer = new MutationObserver(() => {
if (!document.getElementById("maa-copilot-plus")) {
injectToNavbar();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
initUI();
})();