dms-helper

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

Advertisement:

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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"]
		});
	}
})();