820 lines
23 KiB
JavaScript
820 lines
23 KiB
JavaScript
// ==UserScript==
|
||
// @name 云图报告同比复制助手
|
||
// @namespace https://yuntu.oceanengine.com/
|
||
// @version 0.1.0
|
||
// @description 记录最近一次成功创建的云图报告,并一键复制同比报告
|
||
// @author wangxi
|
||
// @match https://yuntu.oceanengine.com/yuntu_brand/ecom/product/segmentedMarketList*
|
||
// @match https://yuntu.oceanengine.com/yuntu_brand/ecom/product/segmentedMarketcreation*
|
||
// @match https://yuntu.oceanengine.com/yuntu_brand/ecom/product/segmentedMarketDetail/*
|
||
// @grant GM_getValue
|
||
// @grant GM_setValue
|
||
// @grant GM_xmlhttpRequest
|
||
// @grant GM_addStyle
|
||
// @grant unsafeWindow
|
||
// @connect localhost
|
||
// @connect 127.0.0.1
|
||
// ==/UserScript==
|
||
|
||
(function bootstrap(root, factory) {
|
||
const api = factory(root);
|
||
if (typeof module === "object" && module.exports) {
|
||
module.exports = api;
|
||
return;
|
||
}
|
||
api.init();
|
||
})(typeof globalThis !== "undefined" ? globalThis : this, function factory(root) {
|
||
const TARGET_PATH = "/product_node/v2/api/segmentedMarket/createSegmentedMarket";
|
||
const PAGE_TYPES = {
|
||
list: "list",
|
||
creation: "creation",
|
||
detail: "detail",
|
||
};
|
||
// 后续如果后端部署到服务器,只需要修改这里的基础地址。
|
||
// 例如改成:https://your-server.example.com
|
||
// 脚本会在这些基础地址后自动拼接 /api/reports。
|
||
const LOCAL_API_BASES = ["http://localhost:3000", "http://127.0.0.1:3000"];
|
||
const SUPPORTED_PAGE_PATTERNS = [
|
||
{
|
||
type: PAGE_TYPES.list,
|
||
pattern: /^\/yuntu_brand\/ecom\/product\/segmentedMarketList(?:\/.*)?$/,
|
||
},
|
||
{
|
||
type: PAGE_TYPES.creation,
|
||
pattern: /^\/yuntu_brand\/ecom\/product\/segmentedMarketcreation(?:\/.*)?$/,
|
||
},
|
||
{
|
||
type: PAGE_TYPES.detail,
|
||
pattern: /^\/yuntu_brand\/ecom\/product\/segmentedMarketDetail\/.*$/,
|
||
},
|
||
];
|
||
const STORAGE_KEYS = {
|
||
payload: "lastReportPayload",
|
||
meta: "lastReportMeta",
|
||
};
|
||
const EVENT_NAME = "yuntu-report-filling:capture";
|
||
const BUTTON_ID = "yuntu-report-filling-action";
|
||
const STYLE_ID = "yuntu-report-filling-style";
|
||
const INJECT_FLAG = "__yuntuReportFillingHooked__";
|
||
|
||
const runtimeState = {
|
||
isSubmitting: false,
|
||
suppressedRequestBody: null,
|
||
clearSuppressionTimer: null,
|
||
captureListenerAttached: false,
|
||
routeWatcherCleanup: null,
|
||
};
|
||
|
||
function log(message, extra) {
|
||
if (extra === undefined) {
|
||
console.log("[YuntuReportFilling]", message);
|
||
return;
|
||
}
|
||
console.log("[YuntuReportFilling]", message, extra);
|
||
}
|
||
|
||
function isTargetCreateReportRequest(url, method) {
|
||
if (String(method || "").toUpperCase() !== "POST") {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
const parsed = new URL(url, root.location && root.location.href ? root.location.href : "https://yuntu.oceanengine.com");
|
||
return parsed.pathname === TARGET_PATH && parsed.searchParams.has("aadvid");
|
||
} catch (_error) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function getSupportedPageType(url) {
|
||
try {
|
||
const parsed = new URL(
|
||
url,
|
||
root.location && root.location.href
|
||
? root.location.href
|
||
: "https://yuntu.oceanengine.com",
|
||
);
|
||
|
||
const matched = SUPPORTED_PAGE_PATTERNS.find(({ pattern }) =>
|
||
pattern.test(parsed.pathname),
|
||
);
|
||
return matched ? matched.type : null;
|
||
} catch (_error) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function isSupportedPageUrl(url) {
|
||
return getSupportedPageType(url) !== null;
|
||
}
|
||
|
||
function getLocalApiBaseCandidates() {
|
||
return [...LOCAL_API_BASES];
|
||
}
|
||
|
||
function extractReportId(responseJson) {
|
||
if (!responseJson || typeof responseJson !== "object") {
|
||
return null;
|
||
}
|
||
|
||
const reportId = responseJson.data && responseJson.data.reportId;
|
||
if (reportId === null || reportId === undefined || reportId === "") {
|
||
return null;
|
||
}
|
||
|
||
return String(reportId);
|
||
}
|
||
|
||
function shiftDateBackOneYear(dateString) {
|
||
const match = String(dateString).match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||
if (!match) {
|
||
throw new Error(`Invalid date string: ${dateString}`);
|
||
}
|
||
|
||
const year = Number(match[1]);
|
||
const month = Number(match[2]);
|
||
const day = Number(match[3]);
|
||
const targetYear = year - 1;
|
||
const lastDay = new Date(Date.UTC(targetYear, month, 0)).getUTCDate();
|
||
const safeDay = Math.min(day, lastDay);
|
||
|
||
return `${targetYear}-${String(month).padStart(2, "0")}-${String(safeDay).padStart(2, "0")}`;
|
||
}
|
||
|
||
function deepCloneJson(value) {
|
||
return JSON.parse(JSON.stringify(value));
|
||
}
|
||
|
||
function extractYear(dateString) {
|
||
return String(dateString).slice(0, 4);
|
||
}
|
||
|
||
function buildAutoCopyName(originalName, startTime, endTime) {
|
||
const baseName = originalName || "";
|
||
return `${baseName}${extractYear(startTime)}-${extractYear(endTime)}`;
|
||
}
|
||
|
||
function buildAutoCopyPayload(payload) {
|
||
const cloned = deepCloneJson(payload);
|
||
cloned.startTime = shiftDateBackOneYear(payload.startTime);
|
||
cloned.endTime = shiftDateBackOneYear(payload.endTime);
|
||
cloned.name = buildAutoCopyName(payload.name, cloned.startTime, cloned.endTime);
|
||
return cloned;
|
||
}
|
||
|
||
function buildPersistRequest(responseJson) {
|
||
return deepCloneJson(responseJson);
|
||
}
|
||
|
||
function parseJson(text) {
|
||
return JSON.parse(text);
|
||
}
|
||
|
||
function getAadvidFromUrl(url) {
|
||
try {
|
||
const parsed = new URL(url, root.location.href);
|
||
return parsed.searchParams.get("aadvid");
|
||
} catch (_error) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function getCreateRequestUrl(meta) {
|
||
if (meta && meta.requestUrl) {
|
||
return meta.requestUrl;
|
||
}
|
||
|
||
const aadvid = (meta && meta.aadvid) || getAadvidFromUrl(root.location.href);
|
||
if (!aadvid) {
|
||
throw new Error("未获取到 aadvid");
|
||
}
|
||
|
||
return `https://yuntu.oceanengine.com${TARGET_PATH}?aadvid=${encodeURIComponent(aadvid)}`;
|
||
}
|
||
|
||
function gmGetValueSafe(key, fallbackValue) {
|
||
if (typeof GM_getValue !== "function") {
|
||
return fallbackValue;
|
||
}
|
||
return GM_getValue(key, fallbackValue);
|
||
}
|
||
|
||
function gmSetValueSafe(key, value) {
|
||
if (typeof GM_setValue !== "function") {
|
||
throw new Error("GM_setValue 不可用");
|
||
}
|
||
GM_setValue(key, value);
|
||
}
|
||
|
||
function readStoredPayload() {
|
||
const rawValue = gmGetValueSafe(STORAGE_KEYS.payload, "");
|
||
if (!rawValue) {
|
||
return null;
|
||
}
|
||
|
||
return parseJson(rawValue);
|
||
}
|
||
|
||
function readStoredMeta() {
|
||
const rawValue = gmGetValueSafe(STORAGE_KEYS.meta, "");
|
||
if (!rawValue) {
|
||
return null;
|
||
}
|
||
|
||
return parseJson(rawValue);
|
||
}
|
||
|
||
function writeStoredData(payload, meta) {
|
||
gmSetValueSafe(STORAGE_KEYS.payload, JSON.stringify(payload));
|
||
gmSetValueSafe(STORAGE_KEYS.meta, JSON.stringify(meta));
|
||
}
|
||
|
||
function clearSuppression() {
|
||
runtimeState.suppressedRequestBody = null;
|
||
if (runtimeState.clearSuppressionTimer) {
|
||
root.clearTimeout(runtimeState.clearSuppressionTimer);
|
||
runtimeState.clearSuppressionTimer = null;
|
||
}
|
||
}
|
||
|
||
function suppressNextCapture(requestBodyText) {
|
||
runtimeState.suppressedRequestBody = requestBodyText;
|
||
if (runtimeState.clearSuppressionTimer) {
|
||
root.clearTimeout(runtimeState.clearSuppressionTimer);
|
||
}
|
||
runtimeState.clearSuppressionTimer = root.setTimeout(() => {
|
||
clearSuppression();
|
||
}, 10000);
|
||
}
|
||
|
||
function showToast(message, type) {
|
||
const toast = root.document.createElement("div");
|
||
toast.className = `yuntu-report-filling-toast is-${type || "info"}`;
|
||
toast.textContent = message;
|
||
root.document.body.appendChild(toast);
|
||
|
||
root.requestAnimationFrame(() => {
|
||
toast.classList.add("is-visible");
|
||
});
|
||
|
||
root.setTimeout(() => {
|
||
toast.classList.remove("is-visible");
|
||
root.setTimeout(() => {
|
||
toast.remove();
|
||
}, 240);
|
||
}, 2200);
|
||
}
|
||
|
||
function updateButtonState() {
|
||
const button = root.document.getElementById(BUTTON_ID);
|
||
if (!button) {
|
||
return;
|
||
}
|
||
|
||
const pageType = getSupportedPageType(root.location && root.location.href ? root.location.href : "");
|
||
const hasPayload = Boolean(readStoredPayload());
|
||
const buttonUiState = getButtonUiState({
|
||
pageType,
|
||
hasPayload,
|
||
isSubmitting: runtimeState.isSubmitting,
|
||
});
|
||
button.disabled = buttonUiState.disabled;
|
||
button.textContent = runtimeState.isSubmitting
|
||
? "创建中..."
|
||
: "一键复制同比报告";
|
||
button.dataset.enabled = String(!buttonUiState.disabled);
|
||
button.dataset.pageType = String(pageType || "");
|
||
}
|
||
|
||
function getButtonUiState({ pageType, hasPayload, isSubmitting }) {
|
||
if (!pageType) {
|
||
return {
|
||
shouldRender: false,
|
||
disabled: true,
|
||
};
|
||
}
|
||
|
||
if (pageType === PAGE_TYPES.list) {
|
||
return {
|
||
shouldRender: true,
|
||
disabled: true,
|
||
};
|
||
}
|
||
|
||
return {
|
||
shouldRender: true,
|
||
disabled: Boolean(isSubmitting || !hasPayload),
|
||
};
|
||
}
|
||
|
||
function removeButton() {
|
||
const button = root.document.getElementById(BUTTON_ID);
|
||
if (button) {
|
||
button.remove();
|
||
}
|
||
}
|
||
|
||
async function persistToLocalServer(payload) {
|
||
const requestBody = JSON.stringify(payload);
|
||
const bases = getLocalApiBaseCandidates();
|
||
let lastError = null;
|
||
|
||
for (const base of bases) {
|
||
try {
|
||
if (typeof GM_xmlhttpRequest !== "function") {
|
||
const response = await fetch(`${base}/api/reports`, {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: requestBody,
|
||
});
|
||
const data = await response.json();
|
||
if (!response.ok || !data.success) {
|
||
throw new Error((data.error && data.error.message) || "本地服务请求失败");
|
||
}
|
||
return data;
|
||
}
|
||
|
||
const data = await new Promise((resolve, reject) => {
|
||
GM_xmlhttpRequest({
|
||
method: "POST",
|
||
url: `${base}/api/reports`,
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
data: requestBody,
|
||
onload(response) {
|
||
try {
|
||
const json = parseJson(response.responseText);
|
||
if (response.status < 200 || response.status >= 300 || !json.success) {
|
||
reject(new Error((json.error && json.error.message) || "本地服务请求失败"));
|
||
return;
|
||
}
|
||
resolve(json);
|
||
} catch (error) {
|
||
reject(error);
|
||
}
|
||
},
|
||
onerror() {
|
||
reject(new Error(`无法连接本地服务: ${base}`));
|
||
},
|
||
});
|
||
});
|
||
|
||
return data;
|
||
} catch (error) {
|
||
lastError = error;
|
||
}
|
||
}
|
||
|
||
throw new Error(
|
||
`本地服务未启动,请先在 server 目录执行 npm start。${lastError ? ` 原始错误: ${lastError.message}` : ""}`,
|
||
);
|
||
}
|
||
|
||
async function createReportThroughPage(url, payload) {
|
||
const pageFetch =
|
||
typeof unsafeWindow !== "undefined" && unsafeWindow.fetch
|
||
? unsafeWindow.fetch.bind(unsafeWindow)
|
||
: root.fetch.bind(root);
|
||
|
||
const requestBodyText = JSON.stringify(payload);
|
||
suppressNextCapture(requestBodyText);
|
||
|
||
const response = await pageFetch(url, {
|
||
method: "POST",
|
||
headers: {
|
||
accept: "application/json, text/plain, */*",
|
||
"content-type": "application/json",
|
||
},
|
||
credentials: "include",
|
||
body: requestBodyText,
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (!response.ok) {
|
||
clearSuppression();
|
||
throw new Error((data && data.message) || `创建报告失败: ${response.status}`);
|
||
}
|
||
|
||
return {
|
||
responseJson: data,
|
||
requestBodyText,
|
||
};
|
||
}
|
||
|
||
async function handleButtonClick() {
|
||
if (runtimeState.isSubmitting) {
|
||
return;
|
||
}
|
||
|
||
const payload = readStoredPayload();
|
||
const meta = readStoredMeta();
|
||
|
||
if (!payload || !meta) {
|
||
updateButtonState();
|
||
showToast("请先手动成功创建一次报告", "error");
|
||
return;
|
||
}
|
||
|
||
runtimeState.isSubmitting = true;
|
||
updateButtonState();
|
||
|
||
try {
|
||
const nextPayload = buildAutoCopyPayload(payload);
|
||
const requestUrl = getCreateRequestUrl(meta);
|
||
const { responseJson } = await createReportThroughPage(requestUrl, nextPayload);
|
||
const reportId = extractReportId(responseJson);
|
||
|
||
if (!reportId) {
|
||
throw new Error("创建成功,但未获取到 reportId");
|
||
}
|
||
|
||
const nextMeta = {
|
||
reportId,
|
||
aadvid: meta.aadvid || getAadvidFromUrl(requestUrl),
|
||
sourceType: "AUTO_COPY",
|
||
capturedAt: new Date().toISOString(),
|
||
requestUrl,
|
||
};
|
||
|
||
writeStoredData(nextPayload, nextMeta);
|
||
await persistToLocalServer(buildPersistRequest(responseJson));
|
||
|
||
showToast(`同比报告创建成功:${reportId}`, "success");
|
||
} catch (error) {
|
||
log("自动创建同比报告失败", error);
|
||
showToast(error.message || "同比报告创建失败", "error");
|
||
} finally {
|
||
runtimeState.isSubmitting = false;
|
||
updateButtonState();
|
||
}
|
||
}
|
||
|
||
async function handleCapturedRequest(detail) {
|
||
if (!detail || !detail.requestBody || !detail.responseText) {
|
||
return;
|
||
}
|
||
|
||
if (runtimeState.suppressedRequestBody && runtimeState.suppressedRequestBody === detail.requestBody) {
|
||
clearSuppression();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const payload = parseJson(detail.requestBody);
|
||
const responseJson = parseJson(detail.responseText);
|
||
const reportId = extractReportId(responseJson);
|
||
if (!reportId) {
|
||
return;
|
||
}
|
||
|
||
const aadvid = getAadvidFromUrl(detail.url) || getAadvidFromUrl(root.location.href);
|
||
if (!aadvid) {
|
||
throw new Error("未从请求中识别到 aadvid");
|
||
}
|
||
|
||
const meta = {
|
||
reportId,
|
||
aadvid,
|
||
sourceType: "MANUAL_CAPTURE",
|
||
capturedAt: new Date().toISOString(),
|
||
requestUrl: detail.url,
|
||
};
|
||
|
||
writeStoredData(payload, meta);
|
||
updateButtonState();
|
||
|
||
await persistToLocalServer(buildPersistRequest(responseJson));
|
||
|
||
showToast(`已记录报告配置:${reportId}`, "success");
|
||
} catch (error) {
|
||
log("记录手动创建报告失败", error);
|
||
showToast(error.message || "记录报告配置失败", "error");
|
||
updateButtonState();
|
||
}
|
||
}
|
||
|
||
function ensureStyles() {
|
||
if (root.document.getElementById(STYLE_ID)) {
|
||
return;
|
||
}
|
||
|
||
const css = `
|
||
#${BUTTON_ID} {
|
||
position: fixed;
|
||
right: 24px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
z-index: 99999;
|
||
width: 148px;
|
||
min-height: 48px;
|
||
padding: 10px 14px;
|
||
border: none;
|
||
border-radius: 14px;
|
||
background: linear-gradient(135deg, #0f766e, #115e59);
|
||
color: #ffffff;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
line-height: 1.4;
|
||
box-shadow: 0 12px 30px rgba(15, 118, 110, 0.28);
|
||
cursor: pointer;
|
||
transition: transform 160ms ease, opacity 160ms ease, box-shadow 160ms ease;
|
||
}
|
||
|
||
#${BUTTON_ID}:hover:not(:disabled) {
|
||
transform: translateY(-50%) translateX(-2px);
|
||
box-shadow: 0 14px 32px rgba(15, 118, 110, 0.34);
|
||
}
|
||
|
||
#${BUTTON_ID}:disabled {
|
||
cursor: not-allowed;
|
||
opacity: 0.52;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.yuntu-report-filling-toast {
|
||
position: fixed;
|
||
top: 24px;
|
||
right: 24px;
|
||
z-index: 100000;
|
||
max-width: 360px;
|
||
padding: 12px 16px;
|
||
border-radius: 12px;
|
||
color: #ffffff;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
box-shadow: 0 14px 36px rgba(15, 23, 42, 0.22);
|
||
opacity: 0;
|
||
transform: translateY(-8px);
|
||
transition: opacity 180ms ease, transform 180ms ease;
|
||
}
|
||
|
||
.yuntu-report-filling-toast.is-visible {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.yuntu-report-filling-toast.is-success {
|
||
background: #166534;
|
||
}
|
||
|
||
.yuntu-report-filling-toast.is-error {
|
||
background: #b91c1c;
|
||
}
|
||
|
||
.yuntu-report-filling-toast.is-info {
|
||
background: #1d4ed8;
|
||
}
|
||
`;
|
||
|
||
if (typeof GM_addStyle === "function") {
|
||
GM_addStyle(css);
|
||
return;
|
||
}
|
||
|
||
const style = root.document.createElement("style");
|
||
style.id = STYLE_ID;
|
||
style.textContent = css;
|
||
root.document.head.appendChild(style);
|
||
}
|
||
|
||
function ensureButton() {
|
||
if (root.document.getElementById(BUTTON_ID)) {
|
||
return;
|
||
}
|
||
|
||
const button = root.document.createElement("button");
|
||
button.id = BUTTON_ID;
|
||
button.type = "button";
|
||
button.textContent = "一键复制同比报告";
|
||
button.addEventListener("click", handleButtonClick);
|
||
root.document.body.appendChild(button);
|
||
updateButtonState();
|
||
}
|
||
|
||
function syncUiForCurrentPage() {
|
||
if (!root.document || !root.document.body) {
|
||
return;
|
||
}
|
||
|
||
const pageType = getSupportedPageType(root.location && root.location.href ? root.location.href : "");
|
||
const hasPayload = Boolean(readStoredPayload());
|
||
const buttonUiState = getButtonUiState({
|
||
pageType,
|
||
hasPayload,
|
||
isSubmitting: runtimeState.isSubmitting,
|
||
});
|
||
|
||
if (!buttonUiState.shouldRender) {
|
||
removeButton();
|
||
return;
|
||
}
|
||
|
||
ensureStyles();
|
||
ensureButton();
|
||
updateButtonState();
|
||
}
|
||
|
||
function createRouteChangeWatcher(targetRoot, onRouteChange) {
|
||
if (!targetRoot || !targetRoot.history || typeof onRouteChange !== "function") {
|
||
return function noop() {};
|
||
}
|
||
|
||
let timerId = null;
|
||
const schedule = () => {
|
||
if (timerId) {
|
||
targetRoot.clearTimeout(timerId);
|
||
}
|
||
timerId = targetRoot.setTimeout(() => {
|
||
timerId = null;
|
||
onRouteChange();
|
||
}, 0);
|
||
};
|
||
|
||
const originalPushState = targetRoot.history.pushState;
|
||
const originalReplaceState = targetRoot.history.replaceState;
|
||
|
||
targetRoot.history.pushState = function patchedPushState() {
|
||
const result = originalPushState.apply(this, arguments);
|
||
schedule();
|
||
return result;
|
||
};
|
||
|
||
targetRoot.history.replaceState = function patchedReplaceState() {
|
||
const result = originalReplaceState.apply(this, arguments);
|
||
schedule();
|
||
return result;
|
||
};
|
||
|
||
targetRoot.addEventListener("popstate", schedule);
|
||
|
||
return function dispose() {
|
||
if (timerId) {
|
||
targetRoot.clearTimeout(timerId);
|
||
timerId = null;
|
||
}
|
||
targetRoot.history.pushState = originalPushState;
|
||
targetRoot.history.replaceState = originalReplaceState;
|
||
targetRoot.removeEventListener("popstate", schedule);
|
||
};
|
||
}
|
||
|
||
function injectCaptureHook() {
|
||
if (!root.document || root.document.documentElement.getAttribute(INJECT_FLAG) === "true") {
|
||
return;
|
||
}
|
||
|
||
const script = root.document.createElement("script");
|
||
script.textContent = `
|
||
(() => {
|
||
if (window.${INJECT_FLAG}) {
|
||
return;
|
||
}
|
||
window.${INJECT_FLAG} = true;
|
||
|
||
const eventName = ${JSON.stringify(EVENT_NAME)};
|
||
const targetPath = ${JSON.stringify(TARGET_PATH)};
|
||
|
||
const isTarget = (url, method) => {
|
||
try {
|
||
const parsed = new URL(url, window.location.href);
|
||
return String(method || "").toUpperCase() === "POST"
|
||
&& parsed.pathname === targetPath
|
||
&& parsed.searchParams.has("aadvid");
|
||
} catch (_error) {
|
||
return false;
|
||
}
|
||
};
|
||
|
||
const serializeBody = (body) => {
|
||
if (typeof body === "string") {
|
||
return body;
|
||
}
|
||
if (!body) {
|
||
return "";
|
||
}
|
||
if (body instanceof URLSearchParams) {
|
||
return body.toString();
|
||
}
|
||
if (body instanceof FormData) {
|
||
return "";
|
||
}
|
||
try {
|
||
return JSON.stringify(body);
|
||
} catch (_error) {
|
||
return "";
|
||
}
|
||
};
|
||
|
||
const dispatchCapture = (detail) => {
|
||
window.dispatchEvent(new CustomEvent(eventName, { detail }));
|
||
};
|
||
|
||
const originalFetch = window.fetch;
|
||
window.fetch = async function patchedFetch(input, init) {
|
||
const requestUrl = input instanceof Request ? input.url : String(input);
|
||
const method = (init && init.method) || (input instanceof Request ? input.method : "GET");
|
||
const requestBody = init && "body" in init ? serializeBody(init.body) : "";
|
||
const response = await originalFetch.apply(this, arguments);
|
||
|
||
if (isTarget(requestUrl, method)) {
|
||
try {
|
||
const cloned = response.clone();
|
||
const responseText = await cloned.text();
|
||
dispatchCapture({
|
||
url: requestUrl,
|
||
method,
|
||
requestBody,
|
||
responseText,
|
||
status: response.status,
|
||
});
|
||
} catch (_error) {}
|
||
}
|
||
|
||
return response;
|
||
};
|
||
|
||
const originalOpen = XMLHttpRequest.prototype.open;
|
||
const originalSend = XMLHttpRequest.prototype.send;
|
||
|
||
XMLHttpRequest.prototype.open = function patchedOpen(method, url) {
|
||
this.__yuntuReportMethod = method;
|
||
this.__yuntuReportUrl = url;
|
||
return originalOpen.apply(this, arguments);
|
||
};
|
||
|
||
XMLHttpRequest.prototype.send = function patchedSend(body) {
|
||
this.__yuntuReportBody = serializeBody(body);
|
||
this.addEventListener("loadend", () => {
|
||
if (!isTarget(this.__yuntuReportUrl, this.__yuntuReportMethod)) {
|
||
return;
|
||
}
|
||
dispatchCapture({
|
||
url: this.__yuntuReportUrl,
|
||
method: this.__yuntuReportMethod,
|
||
requestBody: this.__yuntuReportBody || "",
|
||
responseText: typeof this.responseText === "string" ? this.responseText : "",
|
||
status: this.status,
|
||
});
|
||
}, { once: true });
|
||
return originalSend.apply(this, arguments);
|
||
};
|
||
})();
|
||
`;
|
||
|
||
root.document.documentElement.appendChild(script);
|
||
script.remove();
|
||
root.document.documentElement.setAttribute(INJECT_FLAG, "true");
|
||
}
|
||
|
||
function attachCaptureListener() {
|
||
if (runtimeState.captureListenerAttached) {
|
||
return;
|
||
}
|
||
|
||
root.addEventListener(EVENT_NAME, (event) => {
|
||
handleCapturedRequest(event.detail);
|
||
});
|
||
runtimeState.captureListenerAttached = true;
|
||
}
|
||
|
||
function ensureRouteWatcher() {
|
||
if (runtimeState.routeWatcherCleanup) {
|
||
return;
|
||
}
|
||
|
||
runtimeState.routeWatcherCleanup = createRouteChangeWatcher(root, () => {
|
||
syncUiForCurrentPage();
|
||
});
|
||
}
|
||
|
||
function init() {
|
||
if (!root.document || !root.document.body) {
|
||
root.addEventListener("DOMContentLoaded", init, { once: true });
|
||
return;
|
||
}
|
||
|
||
ensureRouteWatcher();
|
||
attachCaptureListener();
|
||
injectCaptureHook();
|
||
syncUiForCurrentPage();
|
||
}
|
||
|
||
return {
|
||
buildAutoCopyPayload,
|
||
buildAutoCopyName,
|
||
buildPersistRequest,
|
||
createRouteChangeWatcher,
|
||
extractReportId,
|
||
getButtonUiState,
|
||
getLocalApiBaseCandidates,
|
||
getSupportedPageType,
|
||
init,
|
||
isSupportedPageUrl,
|
||
isTargetCreateReportRequest,
|
||
shiftDateBackOneYear,
|
||
};
|
||
});
|