dms-helper

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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