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