dms-helper

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

(function() {
  'use strict';
	var SELECTORS = {
		resultContainer: ".con-sql-result",
		toolbar: ".bar-top",
		table: ".art-table",
		headerRow: ".art-table-header-row",
		bodyRows: ".art-table-body .art-table-row",
		headerText: ".text",
		cellText: ".text",
		activeTabPane: ".next-tabs-tabpane.active"
	};
	function parseTable(resultContainer) {
		const table = (resultContainer ?? document).querySelector(SELECTORS.table);
		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());
		});
		const rows = [];
		table.querySelectorAll(SELECTORS.bodyRows).forEach((rowEl) => {
			const cells = [];
			rowEl.querySelectorAll(".art-table-cell").forEach((cell) => {
				const textEl = cell.querySelector(SELECTORS.cellText);
				const text = textEl ? textEl.textContent : cell.textContent;
				cells.push((text ?? "").trim());
			});
			rows.push(cells);
		});
		return {
			headers,
			rows
		};
	}
	function toCSV(data) {
		if (!data) return "";
		const escape = (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(escape).join(",")];
		data.rows.forEach((row) => lines.push(row.map(escape).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 copyText$1(text, type) {
		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 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 = onClick;
			return btn;
		};
		const csvBtn = createBtn("复制 CSV", () => {
			const data = parseTable(resultContainer);
			if (data) copyText$1(toCSV(data), "CSV");
		});
		csvBtn.id = "dms-helper-csv-btn";
		const mdBtn = createBtn("复制 Markdown", () => {
			const data = parseTable(resultContainer);
			if (data) copyText$1(toMarkdown(data), "Markdown");
		});
		mdBtn.id = "dms-helper-md-btn";
		toolbar.appendChild(csvBtn);
		toolbar.appendChild(mdBtn);
	}
	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(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(`[${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 isInIframe = window.self !== window.top;
	function getActiveResultContainer() {
		const resultAreas = document.querySelectorAll(SELECTORS.resultContainer);
		for (const resultArea of resultAreas) {
			if (resultArea.closest(".next-tabs-tabpane.hidden")) continue;
			return resultArea;
		}
		return null;
	}
	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 (isInIframe) 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 (isInIframe) {
		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"]
		});
	}
})();