dms-helper

阿里云 DMS 查询结果表格复制工具(CSV & Markdown)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name         dms-helper
// @namespace    https://github.com/mudssky/userscripts-monorepo/tree/main/userscripts/dms-helper
// @version      1.4.1
// @author       mudssky
// @description  阿里云 DMS 查询结果表格复制工具(CSV & Markdown)
// @license      MIT
// @icon         https://vitejs.dev/logo.svg
// @homepage     https://github.com/mudssky/userscripts-monorepo/tree/main/userscripts/dms-helper
// @homepageURL  https://github.com/mudssky/userscripts-monorepo/tree/main/userscripts/dms-helper
// @supportURL   https://github.com/mudssky/userscripts-monorepo/issues
// @match        *://dms.aliyun.com/*
// @match        *://dmsnext.console.aliyun.com/_console/sql-console*
// @grant        GM_getValue
// @grant        GM_notification
// @grant        GM_registerMenuCommand
// @grant        GM_setClipboard
// @grant        GM_setValue
// @run-at       document-end
// ==/UserScript==

(function() {
	"use strict";
	var SelectorFailReason = function(SelectorFailReason) {
		SelectorFailReason["NOT_FOUND"] = "NOT_FOUND";
		SelectorFailReason["INVALID_SELECTOR"] = "INVALID_SELECTOR";
		SelectorFailReason["HIDDEN"] = "HIDDEN";
		SelectorFailReason["SHADOW_DOM"] = "SHADOW_DOM";
		SelectorFailReason["IFRAME"] = "IFRAME";
		return SelectorFailReason;
	}({});
	var HTML_SNIPPET_MAX_LENGTH = 200;
	var SIBLINGS_MAX_COUNT = 10;
	function isValidSelector(selector) {
		try {
			document.createDocumentFragment().querySelector(selector);
			return true;
		} catch {
			return false;
		}
	}
	function resolveSelector(name, value, root) {
		if (typeof value === "function") try {
			const element = value(root);
			return {
				name,
				selector: value,
				matched: element !== null,
				count: element !== null ? 1 : 0,
				elements: element ? [element] : [],
				reason: element === null ? SelectorFailReason.NOT_FOUND : void 0
			};
		} catch {
			return {
				name,
				selector: value,
				matched: false,
				count: 0,
				elements: [],
				reason: SelectorFailReason.NOT_FOUND
			};
		}
		if (!isValidSelector(value)) return {
			name,
			selector: value,
			matched: false,
			count: 0,
			elements: [],
			reason: SelectorFailReason.INVALID_SELECTOR
		};
		const elements = Array.from(root.querySelectorAll(value));
		const matched = elements.length > 0;
		const reason = matched ? void 0 : SelectorFailReason.NOT_FOUND;
		return {
			name,
			selector: value,
			matched,
			count: elements.length,
			elements,
			reason
		};
	}
	function debugSelectors(selectors, options = {}) {
		const root = options.root ?? document;
		return Object.entries(selectors).map(([name, value]) => resolveSelector(name, value, root));
	}
	function collectContext(selector, root) {
		const parts = selector.split(/\s+/);
		let nearestMatchedAncestor = null;
		let nearestElement = null;
		for (let i = parts.length - 1; i > 0; i--) {
			const ancestorSelector = parts.slice(0, i).join(" ");
			if (!isValidSelector(ancestorSelector)) continue;
			const found = root.querySelector(ancestorSelector);
			if (found) {
				nearestMatchedAncestor = ancestorSelector;
				nearestElement = found;
				break;
			}
		}
		const siblings = [];
		if (nearestElement?.parentElement) {
			const parent = nearestElement.parentElement;
			for (const child of Array.from(parent.children).slice(0, SIBLINGS_MAX_COUNT)) siblings.push({
				tag: child.tagName.toLowerCase(),
				classes: Array.from(child.classList)
			});
		}
		const parent = nearestElement?.parentElement;
		const nearbyHtmlSnippet = nearestElement?.parentElement ? truncateHtml(nearestElement.parentElement.outerHTML, HTML_SNIPPET_MAX_LENGTH) : null;
		return {
			parentTag: parent?.tagName.toLowerCase() ?? null,
			parentClasses: parent ? Array.from(parent.classList) : [],
			siblings,
			nearestMatchedAncestor,
			nearbyHtmlSnippet
		};
	}
	function generateSuggestion(reason, name) {
		switch (reason) {
			case SelectorFailReason.INVALID_SELECTOR: return `选择器 "${name}" 语法非法,请检查 CSS 选择器拼写`;
			case SelectorFailReason.NOT_FOUND: return `选择器 "${name}" 未匹配到元素。可能原因:元素未加载、选择器过期(页面改版)、在 iframe 或 Shadow DOM 中`;
			case SelectorFailReason.SHADOW_DOM: return `选择器 "${name}" 的目标可能在 Shadow DOM 内`;
			case SelectorFailReason.IFRAME: return `选择器 "${name}" 的目标可能在 iframe 内`;
			default: return "";
		}
	}
	function diagnoseSelectors(selectors, options = {}) {
		const root = options.root ?? document;
		return debugSelectors(selectors, options).map((result) => {
			let context;
			if (!result.matched && typeof result.selector === "string" && result.reason !== SelectorFailReason.INVALID_SELECTOR) context = collectContext(result.selector, root) ?? void 0;
			return {
				name: result.name,
				selector: result.selector,
				matched: result.matched,
				reason: result.reason,
				count: result.count,
				context,
				suggestion: generateSuggestion(result.reason, result.name)
			};
		});
	}
	function formatDiagnostics(diagnostics) {
		const lines = [];
		const total = diagnostics.length;
		const matched = diagnostics.filter((d) => d.matched).length;
		lines.push(`DOM Debug: ${matched}/${total} 选择器匹配`);
		lines.push("─".repeat(40));
		for (const d of diagnostics) {
			const selectorLabel = typeof d.selector === "string" ? d.selector : "[自定义函数]";
			if (d.matched) lines.push(`✓ ${d.name} (${selectorLabel}) — 匹配 ${d.count} 个元素`);
			else {
				lines.push(`✗ ${d.name} (${selectorLabel}) — 未匹配: ${d.reason}`);
				if (d.suggestion) lines.push(`  建议: ${d.suggestion}`);
				if (d.context) {
					if (d.context.nearestMatchedAncestor) lines.push(`  最近匹配祖先: ${d.context.nearestMatchedAncestor}`);
					if (d.context.parentTag) {
						const classStr = d.context.parentClasses.length > 0 ? `.${d.context.parentClasses.join(".")}` : "";
						lines.push(`  父级元素: ${d.context.parentTag}${classStr}`);
					}
				}
			}
		}
		return lines.join("\n");
	}
	function truncateHtml(html, maxLength) {
		if (html.length <= maxLength) return html;
		return `${html.slice(0, maxLength)}...`;
	}
	function dumpDomOutline(root = document.body, maxDepth = 3) {
		const lines = ["页面 DOM 结构概览:", "─".repeat(40)];
		const MAX_CHILDREN = 15;
		function describe(el) {
			return `${el.tagName.toLowerCase()}${el.id ? `#${el.id}` : ""}${el.classList.length > 0 ? `.${Array.from(el.classList).join(".")}` : ""}`.slice(0, 80);
		}
		function walk(el, depth, prefix) {
			if (depth === 0) lines.push(describe(el));
			const childCount = Math.min(el.children.length, MAX_CHILDREN);
			const childPrefix = depth === 0 ? "" : `${prefix}    `;
			for (let i = 0; i < childCount; i++) {
				const connector = i === childCount - 1 && el.children.length <= MAX_CHILDREN ? "└── " : "├── ";
				const child = el.children[i];
				lines.push(`${childPrefix}${connector}${describe(child)}`);
				if (depth + 1 <= maxDepth) walk(child, depth + 1, childPrefix);
			}
			if (el.children.length > MAX_CHILDREN) lines.push(`${childPrefix}└── ... (${el.children.length - MAX_CHILDREN} more)`);
		}
		const startEl = root instanceof Document ? root.body : root;
		if (startEl) walk(startEl, 0, "");
		else lines.push("(document.body 不存在)");
		return lines.join("\n");
	}
	function notify(msg) {
		if (typeof GM_notification !== "undefined") GM_notification({
			text: msg,
			timeout: 4e3
		});
		else console.log(`[notify] ${msg}`);
	}
	function copyText$1(text) {
		if (typeof GM_setClipboard !== "undefined") GM_setClipboard(text);
		else navigator.clipboard.writeText(text).catch(() => {});
	}
	function registerDomDebuggerMenu(options) {
		const { scriptName, selectors, autoDiagnose = false, domDumpDepth = 5 } = options;
		if (typeof GM_registerMenuCommand === "undefined") {
			console.warn(`[${scriptName}] GM_registerMenuCommand 不可用,跳过 DOM Debugger 菜单注册`);
			return;
		}
		const register = (label, action) => {
			GM_registerMenuCommand(label, () => {
				copyText$1(`[${scriptName}] ${action()}`);
				notify(`${scriptName}: 诊断报告已复制到剪贴板`);
				console.log(`[${scriptName}] 报告已复制到剪贴板,详情见下方 ↓`);
			});
		};
		register(`🔍 诊断选择器 (${scriptName})`, () => {
			return `诊断报告:\n${formatDiagnostics(diagnoseSelectors(selectors))}`;
		});
		register(`✅ 快速检测 (${scriptName})`, () => {
			return `快速检测:\n${debugSelectors(selectors).map((r) => {
				const status = r.matched ? `✅ 匹配 (${r.count}个)` : `❌ 未匹配 (${r.reason ?? "unknown"})`;
				return `  ${r.name}: ${status}`;
			}).join("\n")}`;
		});
		register(`📋 DOM 结构 (${scriptName})`, () => {
			return dumpDomOutline(document.body, domDumpDepth);
		});
		if (autoDiagnose) {
			const unmatched = debugSelectors(selectors).filter((r) => !r.matched);
			if (unmatched.length > 0) {
				console.warn(`[${scriptName}] ${unmatched.length}个选择器未匹配:`, unmatched.map((r) => r.name).join(", "));
				console.log(dumpDomOutline(document.body, domDumpDepth));
			}
		}
	}
	var _GM_getValue = (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
	var _GM_notification = (() => typeof GM_notification != "undefined" ? GM_notification : void 0)();
	var _GM_registerMenuCommand = (() => typeof GM_registerMenuCommand != "undefined" ? GM_registerMenuCommand : void 0)();
	var _GM_setClipboard = (() => typeof GM_setClipboard != "undefined" ? GM_setClipboard : void 0)();
	var _GM_setValue = (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
	var COPY_MODE_STORAGE_KEY = "dms-helper-copy-mode";
	function isCopyMode(value) {
		return value === "export" || value === "dom";
	}
	function getCopyMode() {
		if (typeof _GM_getValue === "undefined") return "export";
		const value = _GM_getValue(COPY_MODE_STORAGE_KEY, "export");
		return isCopyMode(value) ? value : "export";
	}
	function setCopyMode(mode) {
		if (typeof _GM_setValue === "undefined") {
			notifyCopyMode("当前环境不支持保存复制模式");
			return;
		}
		_GM_setValue(COPY_MODE_STORAGE_KEY, mode);
	}
	function getNextCopyMode(mode) {
		return mode === "export" ? "dom" : "export";
	}
	function getCopyModeLabel(mode) {
		return mode === "export" ? "导出模式" : "复制模式";
	}
	function registerCopyModeMenu() {
		if (typeof _GM_registerMenuCommand === "undefined") {
			console.warn("[DMS Helper] GM_registerMenuCommand 不可用,跳过复制模式菜单注册");
			return;
		}
		_GM_registerMenuCommand(`切换复制模式(当前:${getCopyModeLabel(getCopyMode())})`, () => {
			const nextMode = getNextCopyMode(getCopyMode());
			setCopyMode(nextMode);
			notifyCopyMode(`已切换为${getCopyModeLabel(nextMode)},刷新页面后菜单文案会更新`);
		});
	}
	function notifyCopyMode(message) {
		if (typeof _GM_notification !== "undefined") {
			_GM_notification({
				title: "DMS Helper",
				text: message,
				timeout: 3e3
			});
			return;
		}
		console.info(`[DMS Helper] ${message}`);
	}
	var SELECTORS = {
		resultContainer: ".con-sql-result, .panel-result",
		toolbar: ".bar-top, .sql-console-results-tab > .next-tabs-bar .next-tabs-nav-extra",
		table: ".art-table, .next-table",
		tableBody: ".art-table-body, .next-table-body",
		tableScrollCandidates: ".art-table-wrapper, .art-table-body, .art-table-body-wrapper, .art-table-scroll, .art-table-container, .art-table-content, .dui-use-virtual, .next-table-body, .next-table-body-wrapper, .next-table-scroller, .next-table-scroll, .next-table-inner, [class*=\"virtual\"], [class*=\"Virtual\"], [class*=\"scroll\"], [class*=\"Scroll\"]",
		headerRow: ".art-table-header-row, .next-table-header tr",
		bodyRows: ".art-table-body .art-table-row, .next-table-body .next-table-row",
		bodyCell: ".art-table-cell, .next-table-cell",
		headerText: ".text, .next-table-cell-wrapper",
		cellText: ".text, .next-table-cell-wrapper",
		activeTabPane: ".next-tabs-tabpane.active"
	};
	function getActiveExecutionResultContext(resultContainer) {
		const tab = findActiveResultTab(resultContainer);
		if (!tab || !isExecutionResultTab(tab)) return null;
		const tabPane = findActiveResultTabPane(resultContainer, tab);
		if (!tabPane) return null;
		return {
			tab,
			tabPane
		};
	}
	function hasActiveExecutionResult(resultContainer) {
		return Boolean(getActiveExecutionResultContext(resultContainer));
	}
	function findActiveExecutionResultTable(resultContainer) {
		return getActiveExecutionResultContext(resultContainer)?.tabPane.querySelector(SELECTORS.table) ?? null;
	}
	function findActiveResultTab(resultContainer) {
		return resultContainer.querySelector(".sql-console-results-tab > .next-tabs-bar [role=\"tab\"].active, .sql-console-results-tab > .next-tabs-bar [role=\"tab\"][aria-selected=\"true\"], .sql-console-results-tab > .next-tabs-bar .next-tabs-tab.active");
	}
	function isExecutionResultTab(tab) {
		const tabText = getElementText$1(tab);
		return /^执行结果\s*\d*$/i.test(tabText) || /^result\s*\d*$/i.test(tabText);
	}
	function findActiveResultTabPane(resultContainer, activeTab) {
		const controlledId = activeTab.getAttribute("aria-controls");
		if (controlledId) {
			const controlledPane = resultContainer.querySelector(`#${CSS.escape(controlledId)}`);
			if (controlledPane) return controlledPane;
		}
		return resultContainer.querySelector(".sql-console-results-tab > .next-tabs-content > .next-tabs-tabpane.active:not(.hidden), .sql-console-results-tab > .next-tabs-content > .next-tabs-tabpane.active");
	}
	function getElementText$1(element) {
		return (element.textContent ?? "").replace(/\s+/g, " ").trim();
	}
	var COPY_CONFIG = {
		slowRowThreshold: 300,
		scrollRenderDelayMs: 80,
		scrollOverlapRows: 2,
		scrollProbeCandidateLimit: 6,
		maxVirtualScrollSteps: 2e3
	};
	function parseCSV(csvText) {
		const [headers, ...rows] = parseCsvRows(csvText);
		if (!headers || headers.length === 0) return null;
		return {
			headers,
			rows
		};
	}
	function parseCsvRows(csvText) {
		const rows = [];
		let row = [];
		let field = "";
		let inQuotes = false;
		for (let index = 0; index < csvText.length; index += 1) {
			const char = csvText[index];
			const nextChar = csvText[index + 1];
			if (char === "\"") {
				if (inQuotes && nextChar === "\"") {
					field += "\"";
					index += 1;
				} else inQuotes = !inQuotes;
				continue;
			}
			if (char === "," && !inQuotes) {
				row.push(field);
				field = "";
				continue;
			}
			if ((char === "\n" || char === "\r") && !inQuotes) {
				if (char === "\r" && nextChar === "\n") index += 1;
				row.push(field);
				rows.push(row);
				row = [];
				field = "";
				continue;
			}
			field += char;
		}
		if (field || row.length > 0) {
			row.push(field);
			rows.push(row);
		}
		return rows;
	}
	function findTable(resultContainer) {
		const container = resultContainer ?? document;
		if (container.querySelector(".sql-console-results-tab")) return findActiveExecutionResultTable(container);
		return container.querySelector(SELECTORS.table);
	}
	function parseHeaders(resultContainer) {
		const table = findTable(resultContainer);
		if (!table) return null;
		const headerEl = table.querySelector(SELECTORS.headerRow);
		if (!headerEl) return null;
		const headers = [];
		headerEl.querySelectorAll("th").forEach((th) => {
			const textEl = th.querySelector(SELECTORS.headerText);
			const text = textEl ? textEl.textContent : th.textContent;
			headers.push((text ?? "").trim());
		});
		return headers;
	}
	function parseVisibleRows(resultContainer) {
		const table = findTable(resultContainer);
		if (!table) return [];
		const rows = [];
		table.querySelectorAll(SELECTORS.bodyRows).forEach((rowEl) => {
			const cells = [];
			rowEl.querySelectorAll(SELECTORS.bodyCell).forEach((cell) => {
				const textEl = cell.querySelector(SELECTORS.cellText);
				const text = textEl ? textEl.textContent : cell.textContent;
				cells.push((text ?? "").trim());
			});
			rows.push(cells);
		});
		return rows;
	}
	function parseTable(resultContainer) {
		const headers = parseHeaders(resultContainer);
		if (!headers) return null;
		return {
			headers,
			rows: parseVisibleRows(resultContainer)
		};
	}
	function toCSV(data) {
		if (!data) return "";
		const escapeCsvCell = (val) => {
			if (val === null || val === void 0) return "";
			const str = String(val);
			if (str.includes(",") || str.includes("\"") || str.includes("\n")) return `"${str.replace(/"/g, "\"\"")}"`;
			return str;
		};
		const lines = [data.headers.map(escapeCsvCell).join(",")];
		data.rows.forEach((row) => {
			lines.push(row.map(escapeCsvCell).join(","));
		});
		return lines.join("\n");
	}
	function toMarkdown(data) {
		if (!data) return "";
		const { headers, rows } = data;
		const escapePipe = (str) => String(str).replace(/\|/g, "\\|");
		return [
			`| ${headers.map(escapePipe).join(" | ")} |`,
			`| ${headers.map(() => "---").join(" | ")} |`,
			...rows.map((row) => `| ${row.map(escapePipe).join(" | ")} |`)
		].join("\n");
	}
	async function buildCopyPayload(resultContainer, format, confirmLargeCopy, dependencies) {
		if (dependencies.getCopyMode() === "dom") return buildDomCopyPayload(resultContainer, format, confirmLargeCopy, dependencies);
		const exportAttempt = await dependencies.exportNativeCsv(resultContainer);
		if (exportAttempt.result) {
			const exportedText = formatExportedCsv(exportAttempt.result.csvText, format);
			if (exportedText) return {
				ok: true,
				text: exportedText,
				copiedType: `${getFormatLabel(format)}(导出)`,
				source: "dms-export-api"
			};
		}
		return buildDomCopyPayload(resultContainer, format, confirmLargeCopy, dependencies, getExportFallbackReason(exportAttempt));
	}
	async function buildDomCopyPayload(resultContainer, format, confirmLargeCopy, dependencies, fallbackReason) {
		const result = await dependencies.collectTableData(resultContainer, {
			rowLimit: COPY_CONFIG.slowRowThreshold,
			confirmLargeCopy
		});
		if (!result.data || result.data.rows.length === 0) return {
			ok: false,
			reason: "未找到可复制的表格数据",
			fallbackReason
		};
		const text = formatTableData(result.data, format);
		if (!text) return {
			ok: false,
			reason: "未找到可复制的表格数据",
			fallbackReason
		};
		const label = getFormatLabel(format);
		return {
			ok: true,
			text,
			copiedType: result.truncated ? `${label}(前 ${result.rowLimit} 行)` : label,
			source: "dom",
			fallbackReason
		};
	}
	function formatExportedCsv(csvText, format) {
		if (format === "csv") return csvText;
		return toMarkdown(parseCSV(csvText));
	}
	function formatTableData(data, format) {
		return format === "csv" ? toCSV(data) : toMarkdown(data);
	}
	function getFormatLabel(format) {
		return format === "csv" ? "CSV" : "Markdown";
	}
	function getExportFallbackReason(exportAttempt) {
		if (exportAttempt.error) return `DMS 导出接口失败:${exportAttempt.error}`;
		if (exportAttempt.exportButtonFound) return "DMS 导出接口未返回 CSV";
		return "未捕获到导出 CSV";
	}
	var DMS_EXPORT_TIMEOUT_MS = 3e4;
	var DMS_EXPORT_EVENT_NAME = "dms-helper:dms-export-result";
	async function exportNativeCsv(resultContainer) {
		if (!canUseNativeExport(resultContainer)) return {
			result: null,
			exportButtonFound: false
		};
		const sql = getResultSql(resultContainer);
		if (!sql) return {
			result: null,
			exportButtonFound: true
		};
		const captureId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
		const result = waitForDmsExportResult(captureId);
		injectDmsExportRunner(captureId, sql);
		const detail = await withTimeout(result, DMS_EXPORT_TIMEOUT_MS);
		if (!detail.success || !detail.csvText) return {
			result: null,
			exportButtonFound: true,
			error: detail.error ?? "DMS 导出接口未返回 CSV"
		};
		return {
			result: {
				csvText: detail.csvText,
				source: "dms-export-api"
			},
			exportButtonFound: true
		};
	}
	function findExportButton(resultContainer) {
		const toolbar = resultContainer.querySelector(SELECTORS.toolbar) ?? document.querySelector(".con-summary .btns") ?? resultContainer;
		return Array.from(toolbar.querySelectorAll("button, [role=\"button\"], a, .next-btn, .next-menu-btn")).find((candidate) => /导出|export|popup-exports/i.test(`${getElementText(candidate)} ${candidate.className}`)) ?? null;
	}
	function canUseNativeExport(resultContainer) {
		if (resultContainer.querySelector(".sql-console-results-tab") && !hasActiveExecutionResult(resultContainer)) return false;
		return Boolean(findExportButton(resultContainer) || getActiveExecutionResultContext(resultContainer)?.tabPane.querySelector(SELECTORS.table));
	}
	function getResultSql(resultContainer) {
		return (getSqlFromResultState(resultContainer) || (resultContainer.matches(".con-sql-result") ? getSqlFromActiveEditor() || getSqlFromTextArea() || getSqlFromCodeMirrorDom() : "")).trim();
	}
	function getSqlFromResultState(resultContainer) {
		const context = getActiveExecutionResultContext(resultContainer);
		return (context ? collectReactSqlCandidates(context.tabPane) : collectReactSqlCandidates(resultContainer)).find((value) => value) ?? "";
	}
	function collectReactSqlCandidates(root) {
		const candidates = [];
		for (const element of [root, ...Array.from(root.querySelectorAll("*"))]) {
			const reactProps = findReactProps(element);
			candidates.push(getNestedString(reactProps, ["data", "executeSQL"]), getNestedString(reactProps, ["record", "executeSQL"]), getNestedString(reactProps, ["result", "executeSQL"]), getNestedString(reactProps, [
				"children",
				"props",
				"data",
				"executeSQL"
			]));
			const fiber = findReactFiber(element);
			candidates.push(...collectSqlFromFiber(fiber));
			if (candidates.some((value) => value)) break;
		}
		return candidates.map((value) => value.trim()).filter((value) => value);
	}
	function getSqlFromActiveEditor() {
		const editor = findEditorLikeObject(window);
		if (!editor) return "";
		try {
			if (typeof editor.getSelection === "function") {
				const selected = editor.getSelection();
				if (typeof selected === "string" && selected.trim()) return selected;
			}
			if (typeof editor.getValue === "function") {
				const value = editor.getValue();
				if (typeof value === "string") return value;
			}
		} catch {
			return "";
		}
		return "";
	}
	function getSqlFromTextArea() {
		return document.querySelector("textarea")?.value ?? "";
	}
	function getSqlFromCodeMirrorDom() {
		return Array.from(document.querySelectorAll(".cm-line, .CodeMirror-line")).map((line) => line.textContent ?? "").filter((line) => line.trim()).join("\n");
	}
	function waitForDmsExportResult(captureId) {
		return new Promise((resolve) => {
			const handler = (event) => {
				if (!(event instanceof CustomEvent)) return;
				if (!isDmsExportEventDetail(event.detail, captureId)) return;
				window.removeEventListener(DMS_EXPORT_EVENT_NAME, handler);
				resolve(event.detail);
			};
			window.addEventListener(DMS_EXPORT_EVENT_NAME, handler);
		});
	}
	function injectDmsExportRunner(captureId, sql) {
		const script = document.createElement("script");
		script.textContent = `
    (async () => {
      const captureId = ${JSON.stringify(captureId)};
      const sql = ${JSON.stringify(sql)};
      const eventName = ${JSON.stringify(DMS_EXPORT_EVENT_NAME)};
      const dispatch = (detail) => window.dispatchEvent(new CustomEvent(eventName, { detail: { captureId, ...detail } }));
      try {
      const readParams = () => {
        const params = new URLSearchParams(window.location.search);
        return {
          dbId: params.get('dbId') || '',
          region: params.get('regionId') || params.get('region') || undefined,
        };
      };
      const getWebpackRequire = () => {
        const chunk = window.webpackChunk_ali_idb_style;
        if (!chunk || typeof chunk.push !== 'function') return null;
        let requireFn = null;
        chunk.push([[Date.now()], {}, (runtimeRequire) => {
          requireFn = runtimeRequire;
        }]);
        return requireFn;
      };
      const getLocalJson = (key) => {
        try {
          return JSON.parse(localStorage.getItem(key) || 'null');
        } catch {
          return null;
        }
      };
      const readRegionFromLocalStorage = (dbId) => {
        for (let index = 0; index < localStorage.length; index += 1) {
          const key = localStorage.key(index) || '';
          if (!key.includes(String(dbId))) continue;
          const value = getLocalJson(key);
          const region = value && (value.idc || value.idcTitle || value.region || value.dataRegion);
          if (typeof region === 'string' && region) return region;
        }
        return '';
      };
      const requestRegionFromDms = async (dbId) => {
        const params = new URLSearchParams(window.location.search);
        const instanceId = params.get('instanceId') || '';
        const dbType = params.get('dbType') || '';
        if (!instanceId || !dbType) return '';
        const body = new URLSearchParams({ dbType, instanceId, dbId });
        const response = await fetch('https://dms.aliyun.com/database/detailInfo', {
          method: 'POST',
          headers: { 'content-type': 'application/x-www-form-urlencoded' },
          body,
          credentials: 'include',
        });
        const payload = await response.json();
        const root = payload && payload.root;
        return (root && (root.idc || root.idcTitle || root.region || root.dataRegion)) || '';
      };
      const readRegion = async (dbId, fallbackRegion) => {
        return fallbackRegion || readRegionFromLocalStorage(dbId) || await requestRegionFromDms(dbId) || 'cn-shanghai';
      };
      const getLanguage = (requireFn) => {
        try {
          const langModule = requireFn(53764);
          return typeof langModule.VQ === 'function' ? langModule.VQ() : 'zh';
        } catch {
          return 'zh';
        }
      };
      const convertRawResultData = (result) => {
        const data = result && result.data;
        const columns = data && data.columnMetaList ? data.columnMetaList.map((column, index) => ({
          field: 'C_' + (index + 1),
          realName: column.columnName,
          title: column.columnLabel || column.columnName,
        })) : [];
        const toRecordRow = (row) => {
          const record = {};
          if (Array.isArray(row)) {
            row.forEach((cell, index) => {
              if (cell !== null && cell !== undefined) record['C_' + (index + 1)] = cell;
            });
            return record;
          }
          if (row && typeof row === 'object') {
            columns.forEach((column) => {
              const cell = row[column.field];
              if (cell !== null && cell !== undefined) record[column.field] = cell;
            });
          }
          return record;
        };
        const rows = data && data.rowDataList ? data.rowDataList.map(toRecordRow) : [];
        return { columns, result: rows };
      };
      const getFieldText = (field) => {
        if (field === null || field === undefined) return '';
        if (typeof field === 'object' && 'value' in field) return String(field.value ?? '');
        return String(field);
      };
      const escapeCsv = (value) => {
        const text = getFieldText(value);
        return /[",\\n\\r]/.test(text) ? '"' + text.replace(/"/g, '""') + '"' : text;
      };
      const toCsv = (resultData) => {
        const columns = resultData && resultData.columns ? resultData.columns : [];
        const rows = resultData && resultData.result ? resultData.result : [];
        const headers = columns.map((column) => column.realName || column.title || column.name || column.field || '');
        const fields = columns.map((column, index) => column.field || ('C_' + (index + 1)));
        return [headers.map(escapeCsv).join(','), ...rows.map((row) => fields.map((field) => escapeCsv(row && row[field])).join(','))].join('\\n');
      };
      const executeByWebpackExecutor = async (dbId, region) => {
        const requireFn = getWebpackRequire();
        if (!requireFn) throw new Error('未找到 DMS webpack 运行时');
        const Executor = requireFn(73623).u0;
        if (typeof Executor !== 'function') throw new Error('未找到 DMS SQL 执行器');
        const executor = new Executor(dbId, getLanguage(requireFn));
        executor.region = region;
        executor.columnTruncate = 0;
        try {
          let hasResult = false;
          await executor.executeSqlsWithCallback(sql, (result) => {
            if (hasResult) return;
            if (result && result.type === 'RESOLVED') return;
            if (!result || result.success === false) {
              hasResult = true;
              dispatch({ success: false, error: (result && (result.error || result.message)) || 'DMS 导出执行失败' });
              return;
            }
            const csvText = toCsv(convertRawResultData(result));
            hasResult = true;
            dispatch({ success: Boolean(csvText), csvText, error: csvText ? undefined : 'DMS 导出结果为空' });
          }, { pageNum: 1 }, { executionAbort: false, ignoreConfirm: true });
          if (!hasResult) dispatch({ success: false, error: 'DMS 导出结果为空' });
        } finally {
          executor.destroy();
        }
      };
      const { dbId, region } = readParams();
      if (!dbId) {
        dispatch({ success: false, error: '未找到 dbId' });
        return;
      }
      const resolvedRegion = await readRegion(dbId, region);
      executeByWebpackExecutor(dbId, resolvedRegion).catch((error) => {
        dispatch({ success: false, error: error instanceof Error ? error.message : String(error) });
      });
      } catch (error) {
        dispatch({ success: false, error: error instanceof Error ? error.message : String(error) });
      }
    })();
  `;
		document.documentElement.appendChild(script);
		script.remove();
	}
	function isDmsExportEventDetail(detail, captureId) {
		if (!detail || typeof detail !== "object") return false;
		const record = detail;
		return record.captureId === captureId && typeof record.success === "boolean";
	}
	function withTimeout(promise, timeoutMs) {
		return new Promise((resolve, reject) => {
			const timer = window.setTimeout(() => reject(new Error("等待 DMS 导出接口超时")), timeoutMs);
			promise.then((value) => {
				window.clearTimeout(timer);
				resolve(value);
			}).catch((error) => {
				window.clearTimeout(timer);
				reject(error);
			});
		});
	}
	function findReactProps(element) {
		const key = Object.keys(element).find((name) => name.startsWith("__reactProps$") || name.startsWith("__reactEventHandlers$"));
		if (!key) return null;
		const value = element[key];
		return isRecord(value) ? value : null;
	}
	function findReactFiber(element) {
		const key = Object.keys(element).find((name) => name.startsWith("__reactFiber$") || name.startsWith("__reactInternalInstance$"));
		if (!key) return null;
		const value = element[key];
		return isRecord(value) ? value : null;
	}
	function collectSqlFromFiber(fiber) {
		const candidates = [];
		let current = fiber;
		for (let depth = 0; isRecord(current) && depth < 20; depth += 1) {
			candidates.push(...collectSqlFromUnknown(current.memoizedProps, 0));
			candidates.push(...collectSqlFromUnknown(current.pendingProps, 0));
			candidates.push(...collectSqlFromUnknown(current.memoizedState, 0));
			current = current.return;
		}
		return candidates;
	}
	function collectSqlFromUnknown(source, depth) {
		if (depth > 5 || !source) return [];
		if (typeof source === "string") return [];
		if (Array.isArray(source)) return source.flatMap((item) => collectSqlFromUnknown(item, depth + 1));
		if (!isRecord(source)) return [];
		const candidates = [];
		for (const [key, value] of Object.entries(source)) {
			if (key === "executeSQL" && typeof value === "string") {
				candidates.push(value);
				continue;
			}
			if (!shouldTraverseKey(key, value)) continue;
			candidates.push(...collectSqlFromUnknown(value, depth + 1));
		}
		return candidates;
	}
	function shouldTraverseKey(key, value) {
		if (!value || typeof value !== "object") return false;
		if (/^(stateNode|child|sibling|return|alternate|ref|elementType|type|_owner)$/i.test(key)) return false;
		return /data|record|result|row|props|state|list|tabs|pane|children|item/i.test(key);
	}
	function getNestedString(source, path) {
		let current = source;
		for (const key of path) {
			if (!isRecord(current)) return "";
			current = current[key];
		}
		return typeof current === "string" ? current : "";
	}
	function findEditorLikeObject(root) {
		const visited = new Set();
		const queue = [root];
		for (let index = 0; index < queue.length && index < 2e3; index += 1) {
			const current = queue[index];
			if (!isRecord(current) || visited.has(current)) continue;
			visited.add(current);
			if (typeof current.getValue === "function" && typeof current.getSelection === "function") return current;
			for (const key of Object.keys(current).slice(0, 80)) {
				if (!/editor|cm|code|view|state|tabs|current/i.test(key)) continue;
				queue.push(current[key]);
			}
		}
		return null;
	}
	function isRecord(value) {
		return Boolean(value) && typeof value === "object";
	}
	function getElementText(element) {
		return (element.textContent ?? "").replace(/\s+/g, " ").trim();
	}
	async function collectTableData(resultContainer, options) {
		const rowLimit = options.rowLimit ?? COPY_CONFIG.slowRowThreshold;
		const scrollContainer = await findVirtualScrollContainer(resultContainer);
		if (!scrollContainer) return collectStaticTableData(resultContainer, rowLimit, options.confirmLargeCopy);
		return collectVirtualTableData(resultContainer, scrollContainer, rowLimit, options.confirmLargeCopy);
	}
	async function collectStaticTableData(resultContainer, rowLimit, confirmLargeCopy) {
		const data = parseTable(resultContainer);
		if (!data) return {
			data,
			truncated: false,
			rowLimit
		};
		const rows = await applyRowLimit(data.rows, rowLimit, confirmLargeCopy);
		return {
			data: {
				headers: data.headers,
				rows: rows.value
			},
			truncated: rows.truncated,
			rowLimit
		};
	}
	async function collectVirtualTableData(resultContainer, scrollContainer, rowLimit, confirmLargeCopy) {
		const headers = parseHeaders(resultContainer);
		if (!headers) return {
			data: null,
			truncated: false,
			rowLimit
		};
		const originalScrollTop = scrollContainer.scrollTop;
		let rows = [];
		let truncated = false;
		let hasConfirmedLargeCopy = false;
		let previousScrollTop = -1;
		try {
			setScrollTop(scrollContainer, 0);
			await waitForTableRender();
			for (let stepIndex = 0; stepIndex < COPY_CONFIG.maxVirtualScrollSteps; stepIndex += 1) {
				const visibleRows = parseVisibleRows(resultContainer);
				rows = mergeRows(rows, visibleRows);
				if (rows.length >= rowLimit && !hasConfirmedLargeCopy && canScrollDown(scrollContainer)) {
					hasConfirmedLargeCopy = await confirmLargeCopy(rowLimit);
					if (!hasConfirmedLargeCopy) {
						rows = rows.slice(0, rowLimit);
						truncated = true;
						break;
					}
				}
				if (!canScrollDown(scrollContainer)) break;
				const nextScrollTop = getNextScrollTop(scrollContainer, resultContainer);
				if (nextScrollTop <= scrollContainer.scrollTop || scrollContainer.scrollTop === previousScrollTop) break;
				previousScrollTop = scrollContainer.scrollTop;
				setScrollTop(scrollContainer, nextScrollTop);
				await waitForTableRender();
			}
			if (rows.length > rowLimit && !hasConfirmedLargeCopy) {
				if (!await confirmLargeCopy(rowLimit)) {
					rows = rows.slice(0, rowLimit);
					truncated = true;
				}
			}
		} finally {
			setScrollTop(scrollContainer, originalScrollTop);
			await waitForTableRender();
		}
		return {
			data: {
				headers,
				rows
			},
			truncated,
			rowLimit
		};
	}
	async function findVirtualScrollContainer(resultContainer) {
		const table = findTable(resultContainer);
		if (!table) return null;
		const candidates = collectScrollCandidates(resultContainer, table).filter((element) => isScrollableElement(element)).map((element) => ({
			element,
			score: scoreScrollCandidate(element, resultContainer, table)
		})).sort((left, right) => right.score - left.score).slice(0, COPY_CONFIG.scrollProbeCandidateLimit);
		for (const candidate of candidates) if (await canTriggerVirtualRows(resultContainer, candidate.element)) return candidate.element;
		return null;
	}
	function collectScrollCandidates(resultContainer, table) {
		const candidates = new Set();
		addElementCandidate(candidates, table);
		resultContainer.querySelectorAll(SELECTORS.tableScrollCandidates).forEach((element) => {
			addElementCandidate(candidates, element);
		});
		resultContainer.querySelectorAll("*").forEach((element) => {
			if (isPotentialScrollCandidate(element, table)) addElementCandidate(candidates, element);
		});
		let current = table.parentElement;
		while (current && current !== resultContainer.parentElement) {
			addElementCandidate(candidates, current);
			if (current === resultContainer) break;
			current = current.parentElement;
		}
		return Array.from(candidates);
	}
	function addElementCandidate(candidates, element) {
		if (element instanceof HTMLElement) candidates.add(element);
	}
	function isPotentialScrollCandidate(element, table) {
		if (!(element instanceof HTMLElement)) return false;
		if (!element.contains(table) && !element.querySelector(SELECTORS.bodyRows)) return false;
		const className = element.className.toString();
		return /table|body|virtual|scroll|container|content/i.test(className) || element.scrollHeight > element.clientHeight + 1;
	}
	function isScrollableElement(element) {
		if (!(element instanceof HTMLElement)) return false;
		if (element.clientHeight <= 0) return false;
		if (element.scrollHeight <= element.clientHeight + 1) return false;
		const overflowY = window.getComputedStyle(element).overflowY;
		return overflowY === "auto" || overflowY === "scroll" || element.matches(SELECTORS.tableScrollCandidates) || canSetScrollTop(element);
	}
	function isResizeDetectorElement(element) {
		return /(^|\s)erd_|resize[-_]?detector|scroll_detection/i.test(element.className.toString());
	}
	function canSetScrollTop(element) {
		if (isResizeDetectorElement(element)) return false;
		const originalScrollTop = element.scrollTop;
		element.scrollTop = originalScrollTop > 0 ? originalScrollTop - 1 : originalScrollTop + 1;
		const changed = element.scrollTop !== originalScrollTop;
		element.scrollTop = originalScrollTop;
		return changed;
	}
	function scoreScrollCandidate(element, resultContainer, table) {
		if (isResizeDetectorElement(element)) return Number.NEGATIVE_INFINITY;
		const className = element.className.toString();
		const scrollRange = element.scrollHeight - element.clientHeight;
		let score = Math.min(scrollRange, 1e4) / 10;
		if (element.matches(SELECTORS.tableBody)) score += 5e3;
		if (element.matches(SELECTORS.tableScrollCandidates)) score += 2e3;
		if (/\bart-table-wrapper\b/i.test(className)) score += 4500;
		if (/\bdui-use-virtual\b/i.test(className)) score += 3500;
		if (/\b(art|next)-table-(body|scroll|scroller|content|container|body-wrapper)\b/i.test(className)) score += 3e3;
		if (/virtual/i.test(className)) score += 1500;
		if (/scroll/i.test(className)) score += 1e3;
		if (element.querySelector(SELECTORS.bodyRows)) score += 800;
		if (element.contains(table)) score += 400;
		if (table.contains(element)) score += 300;
		if (element === resultContainer) score -= 1200;
		return score;
	}
	async function canTriggerVirtualRows(resultContainer, scrollContainer) {
		const originalScrollTop = scrollContainer.scrollTop;
		const originalRows = getVisibleRowSignature(resultContainer);
		const nextScrollTop = getProbeScrollTop(scrollContainer, resultContainer);
		if (nextScrollTop === originalScrollTop) return false;
		try {
			setScrollTop(scrollContainer, nextScrollTop);
			await waitForTableRender();
			const nextRows = getVisibleRowSignature(resultContainer);
			return originalRows.length > 0 && nextRows.length > 0 && !areStringArraysEqual(originalRows, nextRows);
		} finally {
			setScrollTop(scrollContainer, originalScrollTop);
			await waitForTableRender();
		}
	}
	function getProbeScrollTop(scrollContainer, resultContainer) {
		if (canScrollDown(scrollContainer)) return getNextScrollTop(scrollContainer, resultContainer);
		if (scrollContainer.scrollTop > 0) {
			const rowHeight = getEstimatedRowHeight(resultContainer);
			const step = Math.max(rowHeight, scrollContainer.clientHeight - rowHeight * COPY_CONFIG.scrollOverlapRows);
			return Math.max(0, scrollContainer.scrollTop - step);
		}
		return scrollContainer.scrollTop;
	}
	function getVisibleRowSignature(resultContainer) {
		return parseVisibleRows(resultContainer).map((row) => row.join(""));
	}
	function areStringArraysEqual(left, right) {
		if (left.length !== right.length) return false;
		return left.every((value, index) => value === right[index]);
	}
	function canScrollDown(scrollContainer) {
		return scrollContainer.scrollTop + scrollContainer.clientHeight < scrollContainer.scrollHeight - 1;
	}
	function getNextScrollTop(scrollContainer, resultContainer) {
		const rowHeight = getEstimatedRowHeight(resultContainer);
		const overlapHeight = rowHeight * COPY_CONFIG.scrollOverlapRows;
		const step = Math.max(rowHeight, scrollContainer.clientHeight - overlapHeight);
		const maxScrollTop = scrollContainer.scrollHeight - scrollContainer.clientHeight;
		return Math.min(maxScrollTop, scrollContainer.scrollTop + step);
	}
	function setScrollTop(scrollContainer, scrollTop) {
		scrollContainer.scrollTop = scrollTop;
		scrollContainer.dispatchEvent(new Event("scroll", { bubbles: true }));
	}
	function getEstimatedRowHeight(resultContainer) {
		const row = resultContainer.querySelector(SELECTORS.bodyRows);
		if (!(row instanceof HTMLElement)) return 32;
		const height = row.getBoundingClientRect().height;
		return height > 0 ? height : 32;
	}
	function waitForTableRender() {
		return new Promise((resolve) => {
			window.setTimeout(resolve, COPY_CONFIG.scrollRenderDelayMs);
		});
	}
	function mergeRows(collectedRows, visibleRows) {
		if (visibleRows.length === 0) return collectedRows;
		if (collectedRows.length === 0) return [...visibleRows];
		const overlap = findOverlapLength(collectedRows, visibleRows);
		if (overlap === visibleRows.length) return collectedRows;
		return [...collectedRows, ...visibleRows.slice(overlap)];
	}
	function findOverlapLength(collectedRows, visibleRows) {
		const maxOverlap = Math.min(collectedRows.length, visibleRows.length);
		for (let length = maxOverlap; length > 0; length -= 1) if (areRowGroupsEqual(collectedRows.slice(collectedRows.length - length), visibleRows.slice(0, length))) return length;
		return 0;
	}
	function areRowGroupsEqual(leftRows, rightRows) {
		if (leftRows.length !== rightRows.length) return false;
		return leftRows.every((leftRow, index) => areRowsEqual(leftRow, rightRows[index]));
	}
	function areRowsEqual(leftRow, rightRow) {
		if (!rightRow) return false;
		if (leftRow.length !== rightRow.length) return false;
		return leftRow.every((value, index) => value === rightRow[index]);
	}
	async function applyRowLimit(rows, rowLimit, confirmLargeCopy) {
		if (rows.length <= rowLimit) return {
			value: rows,
			truncated: false
		};
		if (await confirmLargeCopy(rowLimit)) return {
			value: rows,
			truncated: false
		};
		return {
			value: rows.slice(0, rowLimit),
			truncated: true
		};
	}
	async function copyText(text, type) {
		if (typeof _GM_setClipboard !== "undefined") try {
			_GM_setClipboard(text, "text");
			showToast(`✅ ${type} 已复制到剪贴板`);
			return;
		} catch {
			console.warn("[DMS Helper] GM_setClipboard 复制失败,尝试浏览器剪贴板");
		}
		try {
			await navigator.clipboard.writeText(text);
			showToast(`✅ ${type} 已复制到剪贴板`);
		} catch {
			const textarea = document.createElement("textarea");
			textarea.value = text;
			textarea.style.position = "fixed";
			textarea.style.opacity = "0";
			document.body.appendChild(textarea);
			textarea.select();
			try {
				document.execCommand("copy");
				showToast(`✅ ${type} 已复制到剪贴板`);
			} catch {
				showToast("❌ 复制失败");
			}
			document.body.removeChild(textarea);
		}
	}
	function showToast(message) {
		document.getElementById("dms-custom-toast")?.remove();
		const toast = document.createElement("div");
		toast.id = "dms-custom-toast";
		toast.textContent = message;
		toast.style.cssText = `
    position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
    background-color: #333; color: #fff; padding: 10px 20px; border-radius: 4px;
    font-size: 14px; z-index: 999999; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
    opacity: 0; transition: opacity 0.3s ease; cursor: pointer;
  `;
		toast.onclick = () => toast.remove();
		document.body.appendChild(toast);
		requestAnimationFrame(() => toast.style.opacity = "1");
		setTimeout(() => {
			toast.style.opacity = "0";
			setTimeout(() => toast.remove(), 300);
		}, 2500);
	}
	function removeInjectedButtons(toolbar) {
		toolbar.querySelectorAll("#dms-helper-csv-btn, #dms-helper-md-btn").forEach((btn) => {
			btn.remove();
		});
	}
	function confirmLargeCopy(rowLimit) {
		return window.confirm(`检测到查询结果超过 ${rowLimit} 行,完整复制可能会比较慢。\n\n点击“确定”继续完整复制,点击“取消”只复制前 ${rowLimit} 行。`);
	}
	async function copyTableWithPreferredMode(resultContainer, format) {
		if (getCopyMode() === "dom") showToast("⚠️ 当前为复制模式,将复制页面 DOM 中已渲染内容");
		else showToast("正在调用 DMS 导出接口...");
		const result = await buildCopyPayload(resultContainer, format, confirmLargeCopy, {
			getCopyMode,
			exportNativeCsv,
			collectTableData
		});
		if (!result.ok) {
			showToast(`❌ ${result.reason}`);
			return;
		}
		if (result.fallbackReason) showToast(`⚠️ ${result.fallbackReason},已切换为复制模式`);
		await copyText(result.text, result.copiedType);
	}
	function injectButtons(toolbar, resultContainer) {
		if (toolbar.querySelector("#dms-helper-csv-btn")) return;
		const createBtn = (text, onClick) => {
			const btn = document.createElement("button");
			btn.className = "next-btn next-small next-btn-normal is-wind";
			btn.style.marginLeft = "8px";
			btn.textContent = text;
			btn.onclick = () => {
				btn.disabled = true;
				onClick().catch(() => showToast("❌ 复制失败")).finally(() => {
					btn.disabled = false;
				});
			};
			return btn;
		};
		const csvBtn = createBtn("复制 CSV", () => copyTableWithPreferredMode(resultContainer, "csv"));
		csvBtn.id = "dms-helper-csv-btn";
		const mdBtn = createBtn("复制 Markdown", () => copyTableWithPreferredMode(resultContainer, "markdown"));
		mdBtn.id = "dms-helper-md-btn";
		toolbar.appendChild(csvBtn);
		toolbar.appendChild(mdBtn);
	}
	var isInIframe = window.self !== window.top;
	var isSqlConsolePage = window.location.hostname === "dmsnext.console.aliyun.com" && window.location.pathname.startsWith("/_console/sql-console");
	var shouldRunInCurrentPage = isInIframe || isSqlConsolePage;
	function getActiveResultContainer() {
		const resultAreas = document.querySelectorAll(SELECTORS.resultContainer);
		for (const resultArea of resultAreas) {
			if (resultArea.closest(".next-tabs-tabpane.hidden")) continue;
			if (isPanelResult(resultArea) && !hasActiveExecutionResult(resultArea)) continue;
			return resultArea;
		}
		return null;
	}
	function isPanelResult(element) {
		return element.matches(".panel-result");
	}
	function checkAndInject() {
		const resultAreas = document.querySelectorAll(SELECTORS.resultContainer);
		const activeResultArea = getActiveResultContainer();
		resultAreas.forEach((resultArea) => {
			const toolbar = resultArea.querySelector(SELECTORS.toolbar);
			if (!toolbar) return;
			if (resultArea === activeResultArea) injectButtons(toolbar, resultArea);
			else removeInjectedButtons(toolbar);
		});
	}
	if (shouldRunInCurrentPage) {
		registerCopyModeMenu();
		registerDomDebuggerMenu({
			scriptName: "DMS Helper",
			selectors: {
				resultContainer: SELECTORS.resultContainer,
				toolbar: SELECTORS.toolbar,
				table: SELECTORS.table,
				headerRow: SELECTORS.headerRow,
				bodyRows: SELECTORS.bodyRows
			},
			autoDiagnose: true,
			domDumpDepth: 6
		});
	}
	if (shouldRunInCurrentPage) {
		checkAndInject();
		let timer;
		const debouncedCheck = () => {
			clearTimeout(timer);
			timer = setTimeout(checkAndInject, 100);
		};
		new MutationObserver(debouncedCheck).observe(document.body, {
			childList: true,
			subtree: true,
			attributes: true,
			attributeFilter: ["class"]
		});
	}
})();