阿里云 DMS 查询结果表格复制工具(CSV & Markdown)
// ==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"]
});
}
})();