// ==UserScript==
// @name 腾讯/钉钉文档表格-隐藏列内容查看器
// @namespace https://docs.qq.com/
// @version 0.4.66
// @description 在腾讯文档与钉钉文档表格页显示列实际内容,并在原网页展开/还原这些列
// @author codex
// @match https://docs.qq.com/sheet/*
// @match https://alidocs.dingtalk.com/i/nodes/*
// @match https://alidocs.dingtalk.com/spreadsheetv2/*
// @grant GM_setClipboard
// @grant unsafeWindow
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const PANEL_ID = 'tm-hidden-col-panel';
const STYLE_ID = 'tm-hidden-col-style';
const LAUNCHER_ID = 'tm-hidden-col-launcher';
const IMAGE_VIEWER_ID = 'tm-hidden-col-image-viewer';
const DEFAULT_TARGET_COLS = [25, 26, 27, 28, 29, 30]; // Z~AE (0-based)
const DINGTALK_DEFAULT_TARGET_COLS = Array.from({ length: 26 }, (_, i) => i); // A~Z
const DINGTALK_ROW_GROUP_SIZE = 3;
const ALIDOCS_HEADER_SCAN_MAX_ROWS = 6;
const ALIDOCS_HEADER_SCAN_MAX_COLS = 36;
const ALIDOCS_HEADER_CACHE_TTL_MS = 8000;
const ALIDOCS_HEADER_KEYWORDS = [
'截图', '违规', '类型', '链接', '店铺', '商品', '品牌', '申诉', '反馈',
'结果', '业务', '责任', '处理', '状态', '时间', '日期', '备注', '渠道',
'价格', 'sku', 'id', '名称', '手机号', '电话', '订单', '平台', '类目',
'型号', '活动', '原因'
];
const PREVIEW_IMAGE_WIDTH = 320;
const QUICK_SCAN_MAX_ROWS = 240;
const QUICK_SCAN_MAX_CELLS = 12000;
const DEEP_SCAN_MAX_ROWS = 6000;
const DEEP_SCAN_MAX_CELLS = 120000;
const SELECTION_RENDER_DEBOUNCE_MS = 90;
const COL_HEADER_RETRY_MAX_ATTEMPTS = 10;
const COL_HEADER_RETRY_INTERVAL_MS = 180;
const COL_ORDER_STORAGE_PREFIX = '__TM_HIDDEN_COL_ORDER__';
const REVEAL_VIEW_STORAGE_PREFIX = '__TM_HIDDEN_COL_REVEAL_VIEW__';
const CELL_READ_OPTIONS = { isIgnoreCustomSheetView: true };
const REVEAL_COL_WIDTH_MIN = 64;
const REVEAL_COL_WIDTH_MAX = 1600;
const REVEAL_ROW_COL_KEY = '__row__';
const REVEAL_DEFAULT_ROW_COL_WIDTH = 76;
const REVEAL_DEFAULT_DATA_COL_WIDTH = 280;
const INIT_RETRY_MAX_ATTEMPTS = 36;
const ALIDOCS_SELECTION_POLL_MS = 160;
const ALIDOCS_PORTAL_PATH_RE = /^\/i\/nodes\//;
const PAGE_WIN = (typeof unsafeWindow !== 'undefined' && unsafeWindow) ? unsafeWindow : window;
const SPREADSHEET_APP_HINT_KEYS = [
'SpreadsheetApp',
'spreadsheetApp',
'__SpreadsheetApp__',
'__sheetApp__',
'AliDocsSpreadsheetApp'
];
const WEBPACK_CHUNK_HINT_KEYS = [
'webpackChunk_tencent_sheet',
'webpackChunk_tencent_docs',
'webpackChunk_alidocs',
'webpackChunk_dingtalk_docs',
'webpackChunk_dingtalk_doc'
];
const DRIVE_URL_CACHE = new Map();
const ALIDOCS_HEADER_ROW_CACHE = new WeakMap();
let SHEET_WEBPACK_REQUIRE = null;
let OPS_FETCH_CHAIN = Promise.resolve();
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function getSheetWebpackRequire() {
if (typeof SHEET_WEBPACK_REQUIRE === 'function') return SHEET_WEBPACK_REQUIRE;
const seen = new Set();
const candidates = [];
const addCandidate = (name, chunk) => {
if (!name || !Array.isArray(chunk) || typeof chunk.push !== 'function') return;
if (seen.has(chunk)) return;
seen.add(chunk);
candidates.push(chunk);
};
for (const key of WEBPACK_CHUNK_HINT_KEYS) {
let chunk = null;
try {
chunk = PAGE_WIN[key];
} catch (_) {
chunk = null;
}
addCandidate(key, chunk);
}
let windowKeys = [];
try {
windowKeys = Object.getOwnPropertyNames(PAGE_WIN);
} catch (_) {
windowKeys = [];
}
for (const key of windowKeys) {
if (!/^webpackChunk/i.test(String(key))) continue;
let chunk = null;
try {
chunk = PAGE_WIN[key];
} catch (_) {
chunk = null;
}
addCandidate(key, chunk);
}
for (const chunk of candidates) {
let req = null;
try {
const marker = `tm_probe_${Date.now()}_${Math.random()}`;
chunk.push([[marker], {}, (r) => { req = r; }]);
} catch (_) {
req = null;
}
if (typeof req === 'function') {
SHEET_WEBPACK_REQUIRE = req;
return SHEET_WEBPACK_REQUIRE;
}
}
return null;
}
function runOpsFetchSerial(task) {
const run = OPS_FETCH_CHAIN.then(task, task);
OPS_FETCH_CHAIN = run.catch(() => { });
return run;
}
async function withTimeout(task, timeoutMs, onTimeoutValue) {
const ms = Math.max(1, Number(timeoutMs) || 1);
let timer = null;
try {
return await Promise.race([
Promise.resolve().then(task),
new Promise((resolve) => {
timer = setTimeout(() => resolve(onTimeoutValue), ms);
})
]);
} finally {
if (timer) clearTimeout(timer);
}
}
async function waitForRevisionRuntime(app, timeoutMs = 4000) {
const started = Date.now();
while (Date.now() - started < timeoutMs) {
const fcs = app?.formulaCalcService;
let calc = null;
try {
calc = fcs?.cellHistoryCalculator;
} catch (_) {
calc = null;
}
if (
fcs &&
calc &&
typeof fcs.startServiceCheck === 'function' &&
typeof calc._cTC === 'function' &&
typeof calc._cTd === 'function' &&
typeof calc.clearCache === 'function'
) {
return { fcs, calc };
}
await sleep(80);
}
return null;
}
function getColumnOrderStorageKey(app) {
const padId = String(PAGE_WIN?.docInfo?.padInfo?.padId || '').trim();
let sheetId = '';
try {
sheetId = String(app?.view?.getActiveCell?.()?.sheetId || '').trim();
} catch (_) { }
if (!sheetId) {
try {
const u = new URL(String(location.href || ''));
sheetId = String(u.searchParams.get('tab') || '').trim();
} catch (_) { }
}
return `${COL_ORDER_STORAGE_PREFIX}:${padId || location.pathname}:${sheetId || 'default'}`;
}
function getRevealViewStorageKey(app) {
const base = String(getColumnOrderStorageKey(app) || '').trim();
const prefix = `${COL_ORDER_STORAGE_PREFIX}:`;
const suffix = base.startsWith(prefix) ? base.slice(prefix.length) : base;
return `${REVEAL_VIEW_STORAGE_PREFIX}:${suffix || location.pathname}`;
}
function loadColumnOrder(key) {
if (!key) return [];
try {
const raw = localStorage.getItem(key);
if (!raw) return [];
const arr = JSON.parse(raw);
if (!Array.isArray(arr)) return [];
const out = [];
const seen = new Set();
for (const x of arr) {
const n = Number(x);
if (!Number.isInteger(n) || seen.has(n)) continue;
seen.add(n);
out.push(n);
}
return out;
} catch (_) {
return [];
}
}
function saveColumnOrder(key, cols) {
if (!key) return;
try {
const out = [];
const seen = new Set();
for (const x of Array.from(cols || [])) {
const n = Number(x);
if (!Number.isInteger(n) || n < 0 || seen.has(n)) continue;
seen.add(n);
out.push(n);
}
localStorage.setItem(key, JSON.stringify(out));
} catch (_) { }
}
function normalizeRevealSortOrder(value) {
return String(value || '').toLowerCase() === 'desc' ? 'desc' : 'asc';
}
function normalizeRevealColWidth(value) {
const n = Number(value);
if (!Number.isFinite(n)) return null;
return Math.max(REVEAL_COL_WIDTH_MIN, Math.min(REVEAL_COL_WIDTH_MAX, Math.round(n)));
}
function loadRevealViewConfig(key) {
const fallback = { sortOrder: 'asc', colWidths: {} };
if (!key) return fallback;
try {
const raw = localStorage.getItem(key);
if (!raw) return fallback;
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') return fallback;
const outWidths = {};
const srcWidths = parsed.colWidths;
if (srcWidths && typeof srcWidths === 'object') {
for (const [k, v] of Object.entries(srcWidths)) {
const keyText = String(k || '').trim();
if (!keyText) continue;
const width = normalizeRevealColWidth(v);
if (width == null) continue;
outWidths[keyText] = width;
}
}
return {
sortOrder: normalizeRevealSortOrder(parsed.sortOrder),
colWidths: outWidths
};
} catch (_) {
return fallback;
}
}
function saveRevealViewConfig(key, config) {
if (!key || !config || typeof config !== 'object') return;
try {
const srcWidths = config.colWidths && typeof config.colWidths === 'object'
? config.colWidths
: {};
const outWidths = {};
for (const [k, v] of Object.entries(srcWidths)) {
const keyText = String(k || '').trim();
if (!keyText) continue;
const width = normalizeRevealColWidth(v);
if (width == null) continue;
outWidths[keyText] = width;
}
const out = {
sortOrder: normalizeRevealSortOrder(config.sortOrder),
colWidths: outWidths
};
localStorage.setItem(key, JSON.stringify(out));
} catch (_) { }
}
function toFiniteInt(value) {
const n = Number(value);
if (!Number.isFinite(n)) return null;
return Math.trunc(n);
}
function normalizeSheetId(value) {
if (value == null) return '';
const s = String(value).trim();
return s;
}
function valueToText(value, depth = 0) {
if (value == null || depth > 4) return '';
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return String(value);
}
if (Array.isArray(value)) {
for (const item of value) {
const t = valueToText(item, depth + 1);
if (t) return t;
}
return '';
}
if (typeof value === 'object') {
const keys = [
'value', 'text', 'formulaResult', 'showValue', 'editValue',
'displayValue', 'formattedText', 'plainText', 'rawText',
'stringValue', 'str', 'cellValue', 'cellText', 'content', 'label'
];
for (const key of keys) {
const t = valueToText(value[key], depth + 1);
if (t) return t;
}
for (const [k, v] of Object.entries(value)) {
if (keys.includes(k)) continue;
const t = valueToText(v, depth + 1);
if (t) return t;
}
}
return '';
}
function looksMaskedDisplayText(text) {
const s = String(text || '').trim();
if (!s) return false;
if (!/^[**·•xX#\s]+$/.test(s)) return false;
return /[**·•xX#]/.test(s);
}
function pickSheetIdFromObject(obj) {
if (!obj || typeof obj !== 'object') return '';
const candidates = [
obj.sheetId,
obj.sheetID,
obj.sheetid,
obj.activeSheetId,
obj.currentSheetId,
obj.sheet?.sheetId,
obj.sheet?.id,
obj.range?.sheetId
];
for (const v of candidates) {
const sid = normalizeSheetId(v);
if (sid) return sid;
}
return '';
}
function normalizeAliDocsCellPos(raw, fallbackSheetId = '') {
if (!raw || typeof raw !== 'object') return null;
const rowIndex = toFiniteInt(raw.rowIndex ?? raw.row ?? raw.r ?? raw.activeRowIndex);
const colIndex = toFiniteInt(raw.colIndex ?? raw.col ?? raw.c ?? raw.activeColIndex);
if (rowIndex == null || colIndex == null) return null;
const sheetId = normalizeSheetId(pickSheetIdFromObject(raw) || fallbackSheetId);
return { rowIndex, colIndex, sheetId };
}
function normalizeAliDocsRange(raw, fallbackSheetId = '') {
if (!raw || typeof raw !== 'object') return null;
const startRowIndex = toFiniteInt(raw.startRowIndex ?? raw.rowIndex ?? raw.row ?? raw.r);
const startColIndex = toFiniteInt(raw.startColIndex ?? raw.colIndex ?? raw.col ?? raw.c);
if (startRowIndex == null || startColIndex == null) return null;
const endRowRaw = raw.endRowIndex ?? raw.rowEndIndex ?? raw.endRow;
const endColRaw = raw.endColIndex ?? raw.colEndIndex ?? raw.endCol;
const endRowIndex = toFiniteInt(endRowRaw);
const endColIndex = toFiniteInt(endColRaw);
const safeEndRow = endRowIndex == null ? startRowIndex + 1 : Math.max(endRowIndex, startRowIndex + 1);
const safeEndCol = endColIndex == null ? startColIndex + 1 : Math.max(endColIndex, startColIndex + 1);
const sheetId = normalizeSheetId(pickSheetIdFromObject(raw) || fallbackSheetId);
return {
sheetId,
startRowIndex,
startColIndex,
endRowIndex: safeEndRow,
endColIndex: safeEndCol
};
}
function readHiddenFlag(node, depth = 0) {
if (node == null || depth > 5) return null;
if (typeof node === 'boolean') return node;
if (typeof node === 'number') {
if (node === 1) return true;
if (node === 0) return false;
return null;
}
if (typeof node === 'string') {
const text = node.trim().toLowerCase();
if (text === 'true' || text === '1') return true;
if (text === 'false' || text === '0') return false;
return null;
}
if (Array.isArray(node)) {
for (const item of node) {
const v = readHiddenFlag(item, depth + 1);
if (typeof v === 'boolean') return v;
}
return null;
}
if (typeof node === 'object') {
const hiddenKeys = [
'hidden', 'isHidden', 'hide', 'isHide', 'colHidden', 'columnHidden',
'hiddenByUser', 'isHiddenByUser'
];
for (const key of hiddenKeys) {
if (!(key in node)) continue;
const v = readHiddenFlag(node[key], depth + 1);
if (typeof v === 'boolean') return v;
}
const widthKeys = ['width', 'colWidth', 'columnWidth'];
for (const key of widthKeys) {
const w = Number(node[key]);
if (Number.isFinite(w) && w <= 0) return true;
}
for (const v of Object.values(node)) {
const hidden = readHiddenFlag(v, depth + 1);
if (typeof hidden === 'boolean') return hidden;
}
}
return null;
}
function parseHiddenFromQueryCols(cols, targetCol) {
if (!Array.isArray(cols) || !cols.length) return null;
let fallback = null;
const pickColIndex = (meta) => {
if (!meta || typeof meta !== 'object') return null;
const keys = ['colIndex', 'columnIndex', 'index', 'col', 'startColIndex'];
for (const key of keys) {
const idx = toFiniteInt(meta[key]);
if (idx != null) return idx;
}
return null;
};
for (const meta of cols) {
const idx = pickColIndex(meta);
const hidden = readHiddenFlag(meta);
if (idx == null) {
if (typeof hidden === 'boolean' && fallback == null) fallback = hidden;
continue;
}
if (idx !== targetCol) continue;
if (typeof hidden === 'boolean') return hidden;
return false;
}
return fallback;
}
function normalizeAliDocsCell(rawCell) {
if (!rawCell || typeof rawCell !== 'object') return null;
const payload = rawCell?.payload && typeof rawCell.payload === 'object' ? rawCell.payload : {};
const showText = valueToText(payload.showValue);
const editText = valueToText(payload.editValue);
const rawText = valueToText(payload.value);
const revealText = looksMaskedDisplayText(showText) && editText ? editText : (rawText || editText || showText);
return {
formattedValue: revealText ? { value: revealText } : '',
value: {
value: rawText || editText || showText || '',
text: revealText || ''
},
payload,
extendedValue: rawCell?.extendedValue,
__tmAliDocsRaw: rawCell,
__tmAliDocsShowValue: showText,
__tmAliDocsEditValue: editText,
__tmAliDocsValue: rawText
};
}
function getReactFiberFromElement(el) {
if (!el || typeof el !== 'object') return null;
let keys = [];
try {
keys = Object.getOwnPropertyNames(el);
} catch (_) {
keys = [];
}
for (const key of keys) {
const k = String(key || '');
if (
!k.startsWith('__reactFiber$') &&
!k.startsWith('__reactContainer$') &&
!k.startsWith('__reactInternalInstance$')
) continue;
try {
const fiber = el[key];
if (fiber && typeof fiber === 'object') return fiber;
} catch (_) { }
}
return null;
}
function isAliDocsControllerCandidate(controller) {
return !!controller &&
typeof controller.getActiveCell === 'function' &&
typeof controller.getSelections === 'function' &&
typeof controller.getSheetRowCount === 'function' &&
typeof controller.getSheetColCount === 'function' &&
typeof controller.getSheetCellValue === 'function';
}
function extractControllerFromFiberNode(node) {
if (!node || typeof node !== 'object') return null;
const candidates = [
node?.memoizedProps?.props?.controller,
node?.pendingProps?.props?.controller,
node?.memoizedProps?.controller,
node?.pendingProps?.controller,
node?.stateNode?.props?.controller,
node?.stateNode?.controller,
node?.memoizedState?.element?.props?.controller
];
for (const controller of candidates) {
if (isAliDocsControllerCandidate(controller)) return controller;
}
return null;
}
function findAliDocsControllerInDocument(doc) {
if (!doc?.documentElement) return null;
const seedFibers = [];
const seenSeed = new Set();
const pushSeedFiber = (fiber) => {
if (!fiber || typeof fiber !== 'object') return;
if (seenSeed.has(fiber)) return;
seenSeed.add(fiber);
seedFibers.push(fiber);
};
const seedElements = [
doc.getElementById('root'),
doc.getElementById('app'),
doc.body,
doc.documentElement
].filter(Boolean);
for (const el of seedElements) {
const fiber = getReactFiberFromElement(el);
if (fiber) pushSeedFiber(fiber);
}
if (!seedFibers.length) {
let all = [];
try {
all = doc.querySelectorAll('*');
} catch (_) {
all = [];
}
const limit = Math.min(all.length, 2400);
for (let i = 0; i < limit; i += 1) {
const fiber = getReactFiberFromElement(all[i]);
if (fiber) pushSeedFiber(fiber);
}
}
if (!seedFibers.length) return null;
const queue = [];
const seen = new Set();
const pushQueue = (fiber) => {
if (!fiber || typeof fiber !== 'object') return;
if (seen.has(fiber)) return;
seen.add(fiber);
queue.push(fiber);
};
for (const fiber of seedFibers) pushQueue(fiber);
const maxVisit = 50000;
for (let i = 0; i < queue.length && i < maxVisit; i += 1) {
const fiber = queue[i];
const controller = extractControllerFromFiberNode(fiber);
if (controller) return controller;
pushQueue(fiber.child);
pushQueue(fiber.sibling);
pushQueue(fiber.return);
pushQueue(fiber.alternate);
const stateNode = fiber.stateNode;
if (stateNode instanceof Element) pushQueue(getReactFiberFromElement(stateNode));
}
return null;
}
function buildAliDocsControllerContext(controller) {
if (!isAliDocsControllerCandidate(controller)) return null;
let cachedSheetId = '';
const resolveSheetId = () => {
const activeCell = (() => {
try { return controller.getActiveCell(); } catch (_) { return null; }
})();
const fromActive = normalizeSheetId(activeCell?.sheetId);
if (fromActive) {
cachedSheetId = fromActive;
return cachedSheetId;
}
let selections = null;
try { selections = controller.getSelections(); } catch (_) { selections = null; }
const list = Array.isArray(selections) ? selections : (selections ? [selections] : []);
for (const item of list) {
const sid = normalizeSheetId(
item?.activeCell?.sheetId ||
item?.rangeSelections?.[0]?.sheetId ||
pickSheetIdFromObject(item)
);
if (!sid) continue;
cachedSheetId = sid;
return cachedSheetId;
}
const fallback = normalizeSheetId(
controller?.activeSheetId ||
controller?.currentSheetId ||
controller?.model?.activeSheetId ||
controller?.model?.currentSheetId ||
controller?.model?.sheetId
);
if (fallback) cachedSheetId = fallback;
return cachedSheetId;
};
const hiddenCache = new Map();
const queryColHidden = (col0) => {
const col = toFiniteInt(col0);
if (col == null || col < 0) return false;
const sheetId = resolveSheetId();
if (!sheetId) return false;
const cacheKey = `${sheetId}:${col}`;
if (hiddenCache.has(cacheKey)) return hiddenCache.get(cacheKey) === true;
let hidden = null;
try {
const dim = controller?.dimensionController;
if (dim && typeof dim.queryCols === 'function') {
const cols = dim.queryCols(sheetId, col, 1);
hidden = parseHiddenFromQueryCols(cols, col);
}
} catch (_) {
hidden = null;
}
if (typeof hidden !== 'boolean') {
try {
hidden = readHiddenFlag(controller?.getShowStyle?.(sheetId, 0, col));
} catch (_) {
hidden = null;
}
}
const finalHidden = hidden === true;
hiddenCache.set(cacheKey, finalHidden);
return finalHidden;
};
const buildSelectionSnapshot = () => {
const sheetId = resolveSheetId();
let activeCell = normalizeAliDocsCellPos((() => {
try { return controller.getActiveCell(); } catch (_) { return null; }
})(), sheetId);
let selections = null;
try { selections = controller.getSelections(); } catch (_) { selections = null; }
const entries = Array.isArray(selections) ? selections : (selections ? [selections] : []);
const ranges = [];
for (const entry of entries) {
const rangeSelections = Array.isArray(entry?.rangeSelections) ? entry.rangeSelections : [];
for (const r of rangeSelections) {
const range = normalizeAliDocsRange(r, sheetId);
if (range) ranges.push(range);
}
if (!activeCell) {
activeCell = normalizeAliDocsCellPos(entry?.activeCell || entry, sheetId);
}
if (!ranges.length) {
const range = normalizeAliDocsRange(entry?.rangeSelection || entry?.range, sheetId);
if (range) ranges.push(range);
}
}
if (!activeCell && ranges.length) {
const r = ranges[0];
activeCell = {
rowIndex: r.startRowIndex,
colIndex: r.startColIndex,
sheetId: normalizeSheetId(r.sheetId || sheetId)
};
}
if (activeCell && !ranges.length) {
ranges.push({
sheetId: normalizeSheetId(activeCell.sheetId || sheetId),
startRowIndex: activeCell.rowIndex,
startColIndex: activeCell.colIndex,
endRowIndex: activeCell.rowIndex + 1,
endColIndex: activeCell.colIndex + 1
});
}
return {
activeCell: activeCell || { rowIndex: 0, colIndex: 0, sheetId: normalizeSheetId(sheetId) },
rangeSelections: ranges
};
};
const selectionKeyOf = (payload) => {
const active = payload?.activeCell || {};
const ranges = Array.isArray(payload?.rangeSelections) ? payload.rangeSelections : [];
const activeKey = [
normalizeSheetId(active.sheetId),
toFiniteInt(active.rowIndex) ?? '',
toFiniteInt(active.colIndex) ?? ''
].join(':');
const rangeKey = ranges.map((r) => [
normalizeSheetId(r?.sheetId),
toFiniteInt(r?.startRowIndex) ?? '',
toFiniteInt(r?.startColIndex) ?? '',
toFiniteInt(r?.endRowIndex) ?? '',
toFiniteInt(r?.endColIndex) ?? ''
].join(':')).join('|');
return `${activeKey}::${rangeKey}`;
};
let sheet = null;
const app = {
__tmProvider: 'alidocs-controller',
__tmAliDocsController: controller,
workbook: {
get activeSheetId() {
return resolveSheetId();
},
get activeSheet() {
return sheet;
}
},
view: {
getActiveCell() {
return buildSelectionSnapshot().activeCell;
},
getSelection() {
return buildSelectionSnapshot();
},
getSelectionRanges() {
return buildSelectionSnapshot().rangeSelections;
},
onSelectionChange(callback) {
if (typeof callback !== 'function') return () => { };
let lastKey = '';
const tick = () => {
const payload = buildSelectionSnapshot();
const nextKey = selectionKeyOf(payload);
if (nextKey === lastKey) return;
lastKey = nextKey;
try { callback(payload); } catch (_) { }
};
tick();
const timer = setInterval(tick, ALIDOCS_SELECTION_POLL_MS);
return () => clearInterval(timer);
}
}
};
sheet = {
__tmProvider: 'alidocs-controller',
__tmAliDocsController: controller,
getSheetId: resolveSheetId,
getRowCount() {
const sheetId = resolveSheetId();
if (!sheetId) return 0;
try {
const n = Number(controller.getSheetRowCount(sheetId));
return Number.isFinite(n) ? Math.max(0, Math.trunc(n)) : 0;
} catch (_) {
return 0;
}
},
getColCount() {
const sheetId = resolveSheetId();
if (!sheetId) return 0;
try {
const n = Number(controller.getSheetColCount(sheetId));
return Number.isFinite(n) ? Math.max(0, Math.trunc(n)) : 0;
} catch (_) {
return 0;
}
},
getCellDataAtPosition(row0, col0) {
const sheetId = resolveSheetId();
if (!sheetId) return null;
const row = Math.max(0, toFiniteInt(row0) || 0);
const col = Math.max(0, toFiniteInt(col0) || 0);
try {
return normalizeAliDocsCell(controller.getSheetCellValue(sheetId, row, col));
} catch (_) {
return null;
}
},
isColHiddenByUser(col0) {
return queryColHidden(col0);
},
isColHidden(col0) {
return queryColHidden(col0);
},
getColProperty(col0) {
const hidden = queryColHidden(col0);
return hidden ? { hidden: true, isHidden: true } : {};
}
};
return { app, sheet };
}
function findAliDocsControllerContext() {
const probes = [];
const seen = new Set();
const pushProbe = (doc) => {
if (!doc || seen.has(doc)) return;
seen.add(doc);
probes.push(doc);
};
pushProbe(document);
let iframes = [];
try {
iframes = document.querySelectorAll('iframe');
} catch (_) {
iframes = [];
}
for (const frame of iframes) {
try {
pushProbe(frame?.contentDocument || null);
} catch (_) { }
}
for (const doc of probes) {
const controller = findAliDocsControllerInDocument(doc);
const ctx = buildAliDocsControllerContext(controller);
if (ctx) return ctx;
}
return null;
}
function isReadySheet(sheet) {
return !!sheet &&
typeof sheet.getCellDataAtPosition === 'function' &&
typeof sheet.getRowCount === 'function' &&
typeof sheet.getColCount === 'function';
}
function pickSpreadsheetContext(candidate) {
if (!candidate || typeof candidate !== 'object') return null;
const sheet = candidate?.workbook?.activeSheet;
if (isReadySheet(sheet)) return { app: candidate, sheet };
return null;
}
function findSpreadsheetContext() {
for (const key of SPREADSHEET_APP_HINT_KEYS) {
let candidate = null;
try {
candidate = PAGE_WIN[key];
} catch (_) {
candidate = null;
}
const ctx = pickSpreadsheetContext(candidate);
if (ctx) return ctx;
}
const containerHints = ['__APP__', '__ALI_DOCS__', 'AliDocs', 'DingTalkDocs'];
for (const name of containerHints) {
let root = null;
try {
root = PAGE_WIN[name];
} catch (_) {
root = null;
}
if (!root || typeof root !== 'object') continue;
for (const key of SPREADSHEET_APP_HINT_KEYS) {
let candidate = null;
try {
candidate = root[key];
} catch (_) {
candidate = null;
}
const ctx = pickSpreadsheetContext(candidate);
if (ctx) return ctx;
}
}
let windowKeys = [];
try {
windowKeys = Object.getOwnPropertyNames(PAGE_WIN);
} catch (_) {
windowKeys = [];
}
for (const key of windowKeys) {
if (!/(spreadsheet|sheet|workbook|grid)/i.test(String(key))) continue;
let candidate = null;
try {
candidate = PAGE_WIN[key];
} catch (_) {
candidate = null;
}
const ctx = pickSpreadsheetContext(candidate);
if (ctx) return ctx;
}
const alidocsCtx = findAliDocsControllerContext();
if (alidocsCtx) return alidocsCtx;
return null;
}
function waitForSpreadsheetApp(timeoutMs = 30000) {
return new Promise((resolve, reject) => {
const started = Date.now();
const timer = setInterval(() => {
const context = findSpreadsheetContext();
if (context) {
clearInterval(timer);
resolve(context);
return;
}
if (Date.now() - started > timeoutMs) {
clearInterval(timer);
reject(new Error('Spreadsheet runtime not ready'));
}
}, 500);
});
}
function colToLetter(col0) {
let n = col0 + 1;
let s = '';
while (n > 0) {
const m = (n - 1) % 26;
s = String.fromCharCode(65 + m) + s;
n = Math.floor((n - 1) / 26);
}
return s;
}
function letterToCol(letter) {
const token = String(letter || '').trim().toUpperCase();
if (!/^[A-Z]+$/.test(token)) return -1;
let n = 0;
for (const ch of token) n = n * 26 + (ch.charCodeAt(0) - 64);
return n - 1;
}
function parseColumnSelection(input, colCount) {
const maxCols = Number(colCount);
const limit = Number.isFinite(maxCols) ? Math.max(0, Math.trunc(maxCols)) : Infinity;
const normalized = String(input || '').toUpperCase().replace(/[,、;;\s]+/g, ',');
const parts = normalized.split(',').map((x) => x.trim()).filter(Boolean);
const out = [];
const seen = new Set();
const addCol = (col) => {
if (!Number.isInteger(col) || col < 0 || col >= limit || seen.has(col)) return;
seen.add(col);
out.push(col);
};
for (const part of parts) {
const m = part.match(/^([A-Z]+)\s*[:\-~]\s*([A-Z]+)$/);
if (m) {
const left = letterToCol(m[1]);
const right = letterToCol(m[2]);
if (!Number.isInteger(left) || !Number.isInteger(right) || left < 0 || right < 0) continue;
const from = Math.min(left, right);
const to = Math.max(left, right);
for (let c = from; c <= to; c += 1) addCol(c);
continue;
}
addCol(letterToCol(part));
}
return out;
}
function displayTextOfCell(cell) {
if (!cell) return '';
const aliShowText = valueToText(cell?.__tmAliDocsShowValue || cell?.payload?.showValue);
if (aliShowText) return aliShowText;
const fv = cell.formattedValue;
if (fv && typeof fv === 'object' && fv.value != null) return String(fv.value);
if (typeof fv === 'string' || typeof fv === 'number' || typeof fv === 'boolean') return String(fv);
const v = cell.value;
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') return String(v);
if (v && typeof v === 'object') {
if (v.text != null) return String(v.text);
if (v.formulaResult != null) return String(v.formulaResult);
if (v.value != null) return String(v.value);
const t = valueToText(v);
if (t) return t;
}
const fallback = valueToText(cell?.payload || cell?.extendedValue || cell?.originData);
if (fallback) return fallback;
return '';
}
function textOfCell(cell) {
if (!cell) return '';
const aliShowText = valueToText(cell?.__tmAliDocsShowValue || cell?.payload?.showValue);
const aliEditText = valueToText(cell?.__tmAliDocsEditValue || cell?.payload?.editValue);
const aliRawText = valueToText(cell?.__tmAliDocsValue || cell?.payload?.value);
if (looksMaskedDisplayText(aliShowText) && aliEditText) return aliEditText;
if (aliRawText) return aliRawText;
if (aliEditText) return aliEditText;
const fv = cell.formattedValue;
if (fv && typeof fv === 'object' && fv.value != null) return String(fv.value);
if (typeof fv === 'string' || typeof fv === 'number' || typeof fv === 'boolean') return String(fv);
const v = cell.value;
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') return String(v);
if (v && typeof v === 'object') {
if (v.value != null) return String(v.value);
if (v.text != null) return String(v.text);
if (v.formulaResult != null) return String(v.formulaResult);
const nested = valueToText(v);
if (nested) return nested;
}
const payloadText = valueToText(cell?.payload || cell?.extendedValue || cell?.originData);
if (payloadText) return payloadText;
return displayTextOfCell(cell);
}
function normalizeCellText(value) {
return String(value == null ? '' : value)
.replace(/[\u200B-\u200D\uFEFF]/g, '')
.replace(/\u00A0/g, ' ')
.trim();
}
function escapeHtml(str) {
return String(str)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('\"', '"')
.replaceAll("'", ''');
}
function getTokXsrf() {
const m = document.cookie.match(/(?:^|;\s*)TOK=([^;]+)/);
return m ? decodeURIComponent(m[1]) : '';
}
function parseDriveObjKey(driveUrl) {
const m = String(driveUrl || '').match(/^drive:\/\/([^/]+)\//);
return m ? m[1] : '';
}
async function resolveDriveUrl(driveUrl) {
const key = String(driveUrl || '').trim();
if (!key) return '';
if (DRIVE_URL_CACHE.has(key)) return DRIVE_URL_CACHE.get(key);
const objKey = parseDriveObjKey(key);
const fileId = PAGE_WIN?.docInfo?.padInfo?.padId || '';
const xsrf = getTokXsrf();
if (!objKey || !fileId || !xsrf) {
DRIVE_URL_CACHE.set(key, key);
return key;
}
try {
const resp = await fetch(`/v2/drive/fileInfo?xsrf=${encodeURIComponent(xsrf)}`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ file_id: fileId, xsrf, obj_key: objKey })
});
const json = await resp.json();
const thumb = json?.result?.fileMeta?.thumbnailUrl || '';
const raw = thumb ? thumb.replace(/&imageMogr2[\s\S]*$/, '') : '';
const finalUrl = raw || key;
DRIVE_URL_CACHE.set(key, finalUrl);
return finalUrl;
} catch (_) {
DRIVE_URL_CACHE.set(key, key);
return key;
}
}
async function resolveUrls(urls) {
if (!Array.isArray(urls) || !urls.length) return [];
const out = [];
for (const u of urls) {
if (!u) continue;
if (/^drive:\/\//i.test(u)) {
out.push(await resolveDriveUrl(u));
} else {
out.push(u);
}
}
return [...new Set(out.filter(Boolean))];
}
function isPreviewableImageUrl(url) {
if (typeof url !== 'string') return false;
const u = url.trim();
if (!u) return false;
if (/^https?:\/\/docimg\d*\.docs\.qq\.com\/image\//i.test(u)) return true;
if (/^https?:\/\/drive\.docs\.qq\.com\//i.test(u)) return true;
if (/\.(png|jpe?g|gif|webp|bmp|svg)([?#].*)?$/i.test(u)) return true;
return false;
}
function valueHtml(item) {
if (Array.isArray(item.resolvedUrls) && item.resolvedUrls.length) {
const imageUrls = item.resolvedUrls.filter(isPreviewableImageUrl);
if (imageUrls.length) {
return `
${imageUrls
.map((u) => {
const s = escapeHtml(u);
return `
`;
})
.join('')}
`;
}
return item.resolvedUrls
.map((u) => `${escapeHtml(u)}`)
.join('
');
}
return escapeHtml(item.value || '');
}
function actionTypeLabel(actionType) {
const n = Number(actionType);
if (n === 1) return '新增';
if (n === 2) return '删除';
if (n === 3) return '修改';
return Number.isFinite(n) ? `类型${n}` : '未知';
}
function formatLocalTime(ts) {
const n = Number(ts);
if (!Number.isFinite(n) || n <= 0) return '';
try {
return new Date(n).toLocaleString('zh-CN', { hour12: false });
} catch (_) {
return '';
}
}
function formatFullDateTime(ts) {
const n = Number(ts);
if (!Number.isFinite(n) || n <= 0) return '';
const d = new Date(n);
const pad2 = (x) => String(x).padStart(2, '0');
const y = d.getFullYear();
const m = pad2(d.getMonth() + 1);
const day = pad2(d.getDate());
const h = pad2(d.getHours());
const min = pad2(d.getMinutes());
const sec = pad2(d.getSeconds());
return `${y}/${m}/${day} ${h}:${min}:${sec}`;
}
function cellDeltaText(delta) {
if (delta == null) return '';
if (typeof delta === 'string' || typeof delta === 'number' || typeof delta === 'boolean') return String(delta);
if (typeof delta !== 'object') return '';
const fv = delta.formattedValue;
if (fv && typeof fv === 'object' && fv.value != null) return String(fv.value);
if (typeof fv === 'string' || typeof fv === 'number' || typeof fv === 'boolean') return String(fv);
const v = delta.value;
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') return String(v);
if (v && typeof v === 'object') {
if (v.value != null) return String(v.value);
if (v.text != null) return String(v.text);
if (v.formulaResult != null) return String(v.formulaResult);
}
return '';
}
function opDetailText(item) {
const before = formatDetailValue(item?.beforeValue);
const after = formatDetailValue(item?.afterValue);
if (before || after) return `${before || '(空)'} -> ${after || '(空)'}`;
return '';
}
function tryFormatExcelSerialDate(text) {
const s = String(text || '').trim();
if (!/^-?\d+(?:\.\d+)?$/.test(s)) return '';
const n = Number(s);
// 仅转换疑似 Excel 序列日期,避免把价格/数量误判成日期。
if (!Number.isFinite(n) || n < 20000 || n > 80000) return '';
const dayMs = 24 * 60 * 60 * 1000;
const excelEpochMs = Date.UTC(1899, 11, 30);
const ts = excelEpochMs + Math.round(n * dayMs);
const dateText = formatLocalTime(ts);
return dateText || '';
}
function formatDetailValue(v) {
const raw = String(v == null ? '' : v).trim();
if (!raw) return '';
const dateText = tryFormatExcelSerialDate(raw);
return dateText || raw;
}
function isNoiseDetailValue(v) {
const text = String(v || '').trim();
if (!text) return true;
return (
/^ClearFormat$/i.test(text) ||
/^ConvertCellTextToNumber$/i.test(text) ||
/^SetCellStyle$/i.test(text) ||
/^SetCellFormat$/i.test(text)
);
}
function usefulDetailText(item) {
const beforeRaw = String(item?.beforeValue || '').trim();
const afterRaw = String(item?.afterValue || '').trim();
const before = formatDetailValue(beforeRaw);
const after = formatDetailValue(afterRaw);
const candidates = [];
if (after && !isNoiseDetailValue(afterRaw)) candidates.push(after);
if (before && !isNoiseDetailValue(beforeRaw) && before !== after) candidates.push(before);
if (!candidates.length) return '';
return [...new Set(candidates)].join(' -> ');
}
function displayDetailText(item) {
return opDetailText(item);
}
function normalizeDetailText(text) {
return String(text || '').replace(/\s+/g, ' ').trim();
}
function dedupeOperationRecords(records) {
const out = [];
const seen = new Set();
for (const item of records || []) {
const detail = normalizeDetailText(displayDetailText(item) || opDetailText(item) || '(空)');
const key = [
Number.isFinite(item?.colIndex0) ? item.colIndex0 : '-',
Number.isFinite(item?.actionType) ? item.actionType : '-',
String(item?.authorId || item?.authorName || '-'),
detail
].join('|');
if (seen.has(key)) continue;
seen.add(key);
out.push(item);
}
return out;
}
function shouldHideNoiseDetailRecord(item) {
const rawText = [
String(item?.beforeValue || ''),
String(item?.afterValue || ''),
opDetailText(item)
].join(' ');
return /ClearFormat|ConvertCellTextToNumber/i.test(rawText);
}
function mutationActionType(typeName) {
const t = String(typeName || '').toLowerCase();
if (/insert|add|create/.test(t)) return 1;
if (/delete|remove|clear/.test(t)) return 2;
return 3;
}
function mutationTypeName(typeMap, type) {
const n = Number(type);
if (!Number.isFinite(n)) return '类型?';
if (typeMap && typeof typeMap === 'object') {
for (const [k, v] of Object.entries(typeMap)) {
if (Number(v) === n) return k;
}
}
return `类型${n}`;
}
function isRowInRange(row0, startRow, endRow) {
const sr = Number(startRow);
const er = Number(endRow);
if (!Number.isFinite(sr) || !Number.isFinite(er)) return false;
if (er > sr) return row0 >= sr && row0 < er;
return row0 === sr;
}
function parseMutationRowCells(mutation, row0, currentSheetId) {
const cells = [];
if (!mutation || typeof mutation !== 'object') return cells;
const expectedSheetId = String(currentSheetId || '').trim();
const g = mutation.gridRangeData || {};
const mutationSheetId = String(
g?.sheetId != null
? g.sheetId
: (typeof mutation.getTargetSheetId === 'function' ? mutation.getTargetSheetId() : '')
).trim();
if (expectedSheetId && mutationSheetId && mutationSheetId !== expectedSheetId) return cells;
if (Array.isArray(mutation.cellAtPositions)) {
for (const at of mutation.cellAtPositions) {
const r = Number(at?.rowIndex);
if (r !== row0) continue;
const c = Number(at?.colIndex);
cells.push({
colIndex0: Number.isFinite(c) ? c : -1,
value: textOfCell(at?.cell)
});
}
}
if (cells.length) return cells;
const sr = Number(g.startRowIndex);
const sc = Number(g.startColIndex);
if (
mutation.cell &&
Number.isFinite(sr) &&
isRowInRange(row0, g.startRowIndex, g.endRowIndex)
) {
cells.push({
colIndex0: Number.isFinite(sc) ? sc : -1,
value: textOfCell(mutation.cell)
});
}
return cells;
}
function fetchCurrentRowOperationRecordsFromLocalQueue(app, row0, sheetId) {
try {
const queue = app?.formulaCalcService?._BLZ?.mutationData?._BBk?.queue;
if (!Array.isArray(queue) || !queue.length) {
return { ok: true, records: [], source: 'localQueue', warning: '未命中本地变更缓存' };
}
const out = [];
const seen = new Set();
for (const item of queue) {
const startTime = Number(item?.startTime || 0);
const mutations = Array.isArray(item?.mutations) ? item.mutations : [];
for (const mut of mutations) {
const rowCells = parseMutationRowCells(mut, row0, sheetId);
if (!rowCells.length) continue;
for (const rc of rowCells) {
const colIndex0 = Number.isFinite(rc?.colIndex0) ? rc.colIndex0 : -1;
const detail = String(rc?.value || '').trim();
const authorId = String(mut?.cell?.author || item?.author || '');
const key = [row0, colIndex0, startTime, Number(mut?.type || 0), authorId, detail].join('|');
if (seen.has(key)) continue;
seen.add(key);
out.push({
rowIndex0: row0,
colIndex0,
modifiedDateTime: startTime,
actionType: 3,
authorId,
authorName: authorId || '-',
beforeValue: '',
afterValue: detail || '本地变更'
});
}
}
}
out.sort((a, b) => (a.modifiedDateTime || 0) - (b.modifiedDateTime || 0));
if (!out.length) {
return { ok: true, records: [], source: 'localQueue', warning: '未命中本地变更缓存' };
}
return { ok: true, records: out, exactCount: out.length, totalCount: out.length, source: 'localQueue' };
} catch (err) {
return { ok: false, error: err?.message || String(err) };
}
}
async function fetchCurrentRowOperationRecordsByChanges({
requireFn, axios, domain, padId, xsrf, row0, latestRev, sheetId
}) {
try {
const revUtils = requireFn(473546);
if (!revUtils?.tq || !revUtils?.oB) {
return { ok: false, error: '变更流解析器不可用' };
}
const mutationMap = requireFn(633935)?.D || {};
let cursor = Number(latestRev);
if (!Number.isFinite(cursor) || cursor <= 0) {
try {
const { data } = await axios.get(`//${domain}/dop-api/get/history`, {
params: { padId, page: 1, _r: Math.floor(Math.random() * 10000), xsrf }
});
cursor = Number(data?.history?.items?.[0]?.i || 0);
} catch (_) {
cursor = 0;
}
}
if (!Number.isFinite(cursor) || cursor <= 0) {
return { ok: false, error: '变更流无可用修订号' };
}
const out = [];
const seen = new Set();
const windowSize = 50;
const maxWindows = 8;
const maxRecords = 120;
const maxDurationMs = 12000;
const startedAt = Date.now();
let to = cursor;
for (
let w = 0;
w < maxWindows &&
to > 0 &&
out.length < maxRecords &&
(Date.now() - startedAt) < maxDurationMs;
w += 1
) {
const from = Math.max(1, to - windowSize + 1);
let result = null;
for (let attempt = 0; attempt < 2 && !result; attempt += 1) {
try {
const { data } = await axios.get(`//${domain}/dop-api/get/changes`, {
params: { from, to, xsrf, padId, needs_author_info: true }
});
result = data?.result || null;
} catch (_) {
if (attempt < 1) await sleep(120);
}
}
const authors = result?.authors || {};
const changes = Array.isArray(result?.changes) ? result.changes : [];
for (const it of changes) {
const d = it?.data || {};
if (!d?.changeset) continue;
let mutations = [];
try {
const rev = revUtils.tq(String(d.changeset || ''));
mutations = revUtils.oB(rev) || [];
} catch (_) {
continue;
}
for (const mut of mutations) {
const rowCells = parseMutationRowCells(mut, row0, sheetId);
if (!rowCells.length) continue;
const typeName = mutationTypeName(mutationMap, mut?.type);
const authorId = String(d?.author || '');
const authorName = String(
authors?.[authorId]?.Name ||
authors?.[authorId]?.name ||
authorId ||
'-'
);
const byCol = new Map();
for (const c of rowCells) {
const key = Number.isFinite(c?.colIndex0) ? c.colIndex0 : -1;
if (!byCol.has(key)) byCol.set(key, []);
byCol.get(key).push(String(c?.value || '').trim());
}
for (const [colIndex0, values] of byCol.entries()) {
const uniqValues = [...new Set(values.filter((x) => x !== ''))];
const detail = uniqValues.length ? uniqValues.join(' | ') : typeName;
const key = [
row0, colIndex0, Number(d?.time || 0), Number(mut?.type || 0), authorId, detail
].join('|');
if (seen.has(key)) continue;
seen.add(key);
out.push({
rowIndex0: row0,
colIndex0,
modifiedDateTime: Number(d?.time || 0),
actionType: mutationActionType(typeName),
authorId,
authorName,
beforeValue: '',
afterValue: detail
});
}
}
}
if (from <= 1) break;
to = from - 1;
}
out.sort((a, b) => (a.modifiedDateTime || 0) - (b.modifiedDateTime || 0));
return {
ok: true,
records: out,
exactCount: out.length,
totalCount: out.length,
source: 'changes'
};
} catch (err) {
return { ok: false, error: err?.message || String(err) };
}
}
function buildOpsUnavailableResult(reason) {
return {
ok: true,
records: [],
exactCount: 0,
totalCount: 0,
source: 'unavailable',
warning: String(reason || '修订接口暂不可用')
};
}
function uniqueValues(values) {
const out = [];
const seen = new Set();
for (const v of values || []) {
const key = `${typeof v}:${String(v)}`;
if (seen.has(key)) continue;
seen.add(key);
out.push(v);
}
return out;
}
function buildSheetIdCandidates(app, sheet) {
const workbook = app?.workbook;
const raw = [
workbook?.activeSheetId,
workbook?.activeSheet?.sheetId,
workbook?.activeSheet?.sheetIdKey,
workbook?.activeSheet?.getSheetId?.(),
sheet?.sheetId,
sheet?.sheetIdKey,
sheet?.getSheetId?.(),
sheet?.model?.sheetId,
sheet?.model?.sheetIdKey,
app?.view?.getActiveCell?.()?.sheetId
];
const expanded = [];
for (const v of raw) {
if (v == null || v === '') continue;
expanded.push(v);
const n = Number(v);
if (Number.isFinite(n)) expanded.push(n);
const s = String(v).trim();
if (s) expanded.push(s);
}
return uniqueValues(expanded);
}
function buildFilterArgVariants(baseArgs, row0, colCount, sheetIdCandidates) {
const rowRanges = [
[row0, row0 + 1], // Tencent 常见区间是 [start, end)
[row0, row0]
];
const colRanges = [
[0, Math.max(0, colCount)],
[0, Math.max(0, colCount - 1)]
];
const out = [];
for (const sid of sheetIdCandidates) {
for (const [sr, er] of rowRanges) {
for (const [sc, ec] of colRanges) {
out.push({
...baseArgs,
filterRangeSheetId: sid,
filterRangeStartRowIndex: sr,
filterRangeStartColIndex: sc,
filterRangeEndRowIndex: er,
filterRangeEndColIndex: ec
});
}
}
}
return out;
}
function isOpsUnavailableError(errorText) {
const t = String(errorText || '');
if (!t) return false;
return (
/_cTC\(10020\)/.test(t) ||
/_cTd\(10040\)/.test(t) ||
/修订服务未就绪/.test(t) ||
/历史接口不可用/.test(t)
);
}
async function fetchCurrentRowOperationRecords(app, sheet, row0) {
try {
if (app?.__tmProvider === 'alidocs-controller' || sheet?.__tmProvider === 'alidocs-controller') {
return buildOpsUnavailableResult('当前文档暂不支持读取修订接口');
}
const requireFn = getSheetWebpackRequire();
const runtime = await waitForRevisionRuntime(app, 4500);
const fcs = runtime?.fcs;
const calc = runtime?.calc;
if (typeof requireFn !== 'function' || !fcs || !calc) {
return { ok: false, error: '修订服务未就绪' };
}
const axiosMod = requireFn(430327);
const axios = axiosMod?.default || axiosMod;
const cfgMod = requireFn(841569);
const globalCfgMod = requireFn(535512);
const xsrfMod = requireFn(281194);
if (!axios?.get) return { ok: false, error: '历史接口不可用' };
const domain = cfgMod?.Z?.getConfig?.('runtime.domainName') || location.host;
const padId = globalCfgMod?.q?.getConfig?.('globalPadId') || PAGE_WIN?.docInfo?.padInfo?.padId || '';
const xsrf = xsrfMod?.f?.() || getTokXsrf();
if (!padId || !xsrf) return { ok: false, error: '缺少文档身份信息' };
let latestRev = null;
try {
const { data } = await axios.get(`//${domain}/dop-api/get/history`, {
params: { padId, page: 1, _r: Math.floor(Math.random() * 10000), xsrf }
});
const rev = Number(data?.history?.items?.[0]?.i);
if (Number.isFinite(rev) && rev > 0) latestRev = rev;
} catch (_) { }
const workbook = app?.workbook;
const sheetIdCandidates = buildSheetIdCandidates(app, sheet);
const sheetId = sheetIdCandidates[0];
const colCount = sheet.getColCount?.() || 1;
if (sheetId == null || sheetId === '') return { ok: false, error: '工作表上下文不可用' };
const tryChangesFallback = async () => fetchCurrentRowOperationRecordsByChanges({
requireFn, axios, domain, padId, xsrf, row0, latestRev, sheetId
});
const tryLocalQueueFallback = async () => fetchCurrentRowOperationRecordsFromLocalQueue(app, row0, sheetId);
let preferredFilterShape = null;
const createBaseFilterArgs = () => ({
// 线上实测:使用 0 作为起始时间更稳定,避免 _cTC(10020)
recordStartTime: 0,
recordEndTime: Date.now(),
filterLimit: 300
});
const runFilterQuery = async () => {
const baseFilterArgs = createBaseFilterArgs();
const argsVariants = [];
const seenVariant = new Set();
const pushArgs = (args) => {
if (!args || typeof args !== 'object') return;
const key = [
String(args.filterRangeSheetId),
args.filterRangeStartRowIndex,
args.filterRangeEndRowIndex,
args.filterRangeStartColIndex,
args.filterRangeEndColIndex
].join('|');
if (seenVariant.has(key)) return;
seenVariant.add(key);
argsVariants.push(args);
};
if (preferredFilterShape) {
pushArgs({ ...baseFilterArgs, ...preferredFilterShape });
}
for (const args of buildFilterArgVariants(baseFilterArgs, row0, colCount, sheetIdCandidates)) {
pushArgs(args);
}
let lastError = null;
let lastArgs = null;
for (const args of argsVariants) {
lastArgs = args;
const step1 = await calc._cTC(args);
if (!step1?.isError) {
preferredFilterShape = {
filterRangeSheetId: args.filterRangeSheetId,
filterRangeStartRowIndex: args.filterRangeStartRowIndex,
filterRangeStartColIndex: args.filterRangeStartColIndex,
filterRangeEndRowIndex: args.filterRangeEndRowIndex,
filterRangeEndColIndex: args.filterRangeEndColIndex
};
return { ok: true, step1, args };
}
lastError = step1;
const code = Number(step1?.code);
// 参数类错误可继续尝试其它组合;其它错误先保留,仍允许后续组合兜底。
if (code !== 10020 && code !== 10040) {
continue;
}
}
return { ok: false, error: lastError || { isError: true, code: 'UNKNOWN' }, args: lastArgs };
};
const resetAndPrime = async () => {
await calc.clearCache();
if (typeof calc._cTD === 'function') {
const primeRev = Number.isFinite(Number(latestRev)) && Number(latestRev) > 0 ? Number(latestRev) : 1;
try { await calc._cTD(primeRev); } catch (_) { }
}
};
try { await fcs.startServiceCheck(sheetId); } catch (_) { }
await resetAndPrime();
const maxLoops = 16;
const cellHistoryStartedAt = Date.now();
const cellHistoryBudgetMs = 9000;
const seenTrace = new Set();
const all = [];
let lastSignature = '';
let invalidTraceRetry = 0;
let filterRuntimeRetry = 0;
const constructResult = async (traceVersion, step1Data, activeSheetHint) => {
const key = step1Data?.currentSearchGridRange?.sheetIdKey;
const activeSheetCandidates = uniqueValues([
activeSheetHint,
sheetId,
Number.isFinite(Number(key)) ? Number(key) : null
]);
let lastError = null;
for (const sid of activeSheetCandidates) {
let s2 = null;
try {
s2 = await calc._cTd({ traceVersion, activeSheetId: sid });
} catch (_) {
s2 = { isError: true, code: 'EXCEPTION' };
}
if (!s2?.isError) return s2;
lastError = s2;
if (Number(s2?.code) !== 10040) break;
}
return lastError || { isError: true, code: 'UNKNOWN' };
};
let activeSheetIdForTd = sheetId;
for (let i = 0; i < maxLoops; i += 1) {
if ((Date.now() - cellHistoryStartedAt) > cellHistoryBudgetMs) break;
let filterRun = await runFilterQuery();
let step1 = filterRun?.step1;
if (!filterRun?.ok || step1?.isError) {
const code = Number(filterRun?.error?.code || step1?.code);
if (code === 10020 && filterRuntimeRetry < 3) {
filterRuntimeRetry += 1;
try { await fcs.startServiceCheck(sheetId); } catch (_) { }
await resetAndPrime();
await sleep(80 * filterRuntimeRetry);
continue;
}
const fallback = await tryChangesFallback();
if (fallback?.ok) return fallback;
const localFallback = await tryLocalQueueFallback();
if (localFallback?.ok && Array.isArray(localFallback.records) && localFallback.records.length) return localFallback;
if (code === 10020) {
return buildOpsUnavailableResult('修订接口暂不可用(_cTC:10020)');
}
return { ok: false, error: `_cTC(${code || ''})` };
}
activeSheetIdForTd = filterRun?.args?.filterRangeSheetId ?? activeSheetIdForTd;
filterRuntimeRetry = 0;
let traceVersion = Number(step1?.data?.traceVersion);
const filterCount = Number(step1?.data?.filterCount || 0);
if (!Number.isFinite(traceVersion) || traceVersion <= 0) {
if (filterCount > 0 && invalidTraceRetry < 3) {
invalidTraceRetry += 1;
await resetAndPrime();
await sleep(80 * invalidTraceRetry);
continue;
}
if (step1?.shouldContinueFilter && i < maxLoops - 1) {
await sleep(40);
continue;
}
break;
}
invalidTraceRetry = 0;
if (seenTrace.has(traceVersion)) {
if (step1?.shouldContinueFilter && i < maxLoops - 1) {
await sleep(30);
continue;
}
break;
}
seenTrace.add(traceVersion);
let step2 = await constructResult(traceVersion, step1?.data, activeSheetIdForTd);
let retryTd10040 = 0;
while (step2?.isError && Number(step2?.code) === 10040 && retryTd10040 < 4) {
retryTd10040 += 1;
await resetAndPrime();
await sleep(50 * retryTd10040);
filterRun = await runFilterQuery();
step1 = filterRun?.step1;
if (!filterRun?.ok || step1?.isError) {
const code = Number(filterRun?.error?.code || step1?.code);
if (code === 10020 && filterRuntimeRetry < 3) {
filterRuntimeRetry += 1;
try { await fcs.startServiceCheck(sheetId); } catch (_) { }
await resetAndPrime();
await sleep(80 * filterRuntimeRetry);
continue;
}
const fallback = await tryChangesFallback();
if (fallback?.ok) return fallback;
const localFallback = await tryLocalQueueFallback();
if (localFallback?.ok && Array.isArray(localFallback.records) && localFallback.records.length) return localFallback;
if (code === 10020) {
return buildOpsUnavailableResult('修订接口暂不可用(_cTC:10020)');
}
return { ok: false, error: `_cTC(${code || ''})` };
}
activeSheetIdForTd = filterRun?.args?.filterRangeSheetId ?? activeSheetIdForTd;
filterRuntimeRetry = 0;
traceVersion = Number(step1?.data?.traceVersion);
if (!Number.isFinite(traceVersion) || traceVersion <= 0) {
step2 = { isError: true, code: 10040 };
break;
}
if (seenTrace.has(traceVersion)) {
step2 = { isError: true, code: 10040 };
break;
}
seenTrace.add(traceVersion);
step2 = await constructResult(traceVersion, step1?.data, activeSheetIdForTd);
}
if (step2?.isError) {
// 10040 多见于 trace 已失效;尝试继续下一轮拉取,不立即中断。
if (Number(step2?.code) === 10040) {
if (all.length) break;
if (i < maxLoops - 1) {
await sleep(60);
continue;
}
}
const fallback = await tryChangesFallback();
if (fallback?.ok) return fallback;
const localFallback = await tryLocalQueueFallback();
if (localFallback?.ok && Array.isArray(localFallback.records) && localFallback.records.length) return localFallback;
return { ok: false, error: `_cTd(${step2?.code || ''})` };
}
const list = Array.isArray(step2?.data?.cellHistoryList) ? step2.data.cellHistoryList : [];
const authorMap = step2?.data?.authorMap || {};
for (const x of list) {
const authorId = String(x?.author || '');
all.push({
rowIndex0: Number(x?.rowIndex),
colIndex0: Number(x?.colIndex),
modifiedDateTime: Number(x?.modifiedDateTime),
actionType: Number(x?.actionType),
authorId,
authorName: authorMap?.[authorId]?.name || authorId,
beforeValue: cellDeltaText(x?.beforeCellDelta),
afterValue: cellDeltaText(x?.afterCellDelta)
});
}
const sig = `${traceVersion}|${list.length}|${step1?.shouldContinueFilter ? 1 : 0}`;
if (sig === lastSignature) break;
lastSignature = sig;
if (!step1?.shouldContinueFilter) break;
}
const uniq = [];
const seen = new Set();
for (const x of all) {
const key = [
x.rowIndex0,
x.colIndex0,
x.modifiedDateTime,
x.actionType,
x.authorId,
x.beforeValue,
x.afterValue
].join('|');
if (seen.has(key)) continue;
seen.add(key);
uniq.push(x);
}
uniq.sort((a, b) => (a.modifiedDateTime || 0) - (b.modifiedDateTime || 0));
const exact = uniq.filter((x) => x.rowIndex0 === row0);
if (!exact.length) {
const fallback = await tryChangesFallback();
if (fallback?.ok) return fallback;
const localFallback = await tryLocalQueueFallback();
if (localFallback?.ok && Array.isArray(localFallback.records) && localFallback.records.length) return localFallback;
return { ok: true, records: [], exactCount: 0, totalCount: uniq.length, source: 'cellHistory' };
}
return { ok: true, records: exact, exactCount: exact.length, totalCount: uniq.length, source: 'cellHistory' };
} catch (err) {
return { ok: false, error: err?.message || String(err) };
}
}
function extractCellUrls(cell) {
const urls = [];
const seen = new Set();
const pushUrl = (u) => {
if (typeof u !== 'string') return;
const t = u.trim();
if (!t) return;
if (!/^(https?:\/\/|drive:\/\/)/i.test(t)) return;
if (seen.has(t)) return;
seen.add(t);
urls.push(t);
};
const collectFromString = (raw) => {
if (typeof raw !== 'string') return;
const s = raw.replace(/\\\//g, '/');
const re = /(drive:\/\/[^\s"'\\\],]+|https?:\/\/[^\s"'\\\],]+)/gi;
let m = null;
while ((m = re.exec(s)) !== null) {
if (m[1]) pushUrl(m[1]);
}
};
const collectDeep = (node, depth = 0) => {
if (node == null || depth > 6) return;
if (typeof node === 'string') {
collectFromString(node);
return;
}
if (Array.isArray(node)) {
for (const x of node) collectDeep(x, depth + 1);
return;
}
if (typeof node === 'object') {
for (const v of Object.values(node)) collectDeep(v, depth + 1);
}
};
// Rich text link segments
if (cell && cell.value && typeof cell.value === 'object') {
if (Array.isArray(cell.value.r)) {
for (const seg of cell.value.r) {
pushUrl(seg?.hyperlink?.url);
}
}
// Fallback for plain hyperlink object
pushUrl(cell.value?.hyperlink?.url);
}
// Image-like cell payload used by Tencent Docs (e.g. AA~AC screenshots)
const ext = cell?.extendedValue;
if (typeof ext === 'string' && ext.trim()) {
// Try decode up to 2 layers for encoded JSON string payloads.
let node = ext;
for (let i = 0; i < 2; i += 1) {
if (typeof node !== 'string') break;
const t = node.trim();
if (!t) break;
if (!/^[\[{"]/.test(t)) break;
try {
node = JSON.parse(t);
} catch (_) {
break;
}
}
collectDeep(node);
} else {
collectDeep(ext);
}
// Last fallback: deep scan known cell payload sections.
collectDeep(cell?.value);
collectDeep(cell?.formattedValue);
collectDeep(cell?.payload);
collectDeep(cell?.__tmAliDocsRaw);
return urls;
}
function isColHidden(sheet, c) {
try {
if (typeof sheet.isColHiddenByUser === 'function' && sheet.isColHiddenByUser(c)) return true;
} catch (_) { }
try {
if (typeof sheet.isColHidden === 'function' && sheet.isColHidden(c)) return true;
} catch (_) { }
try {
if (typeof sheet.getColWidthWithDefault === 'function') {
const w = sheet.getColWidthWithDefault(c);
if (typeof w === 'number' && w <= 0) return true;
}
} catch (_) { }
try {
if (typeof sheet.getColProperty === 'function') {
const p = sheet.getColProperty(c);
if (p && (p.hidden === true || p.isHidden === true)) return true;
}
} catch (_) { }
return false;
}
function isRowHidden(sheet, row0) {
const r = Math.max(0, Math.trunc(Number(row0) || 0));
try {
if (typeof sheet.isRowHiddenByUser === 'function' && sheet.isRowHiddenByUser(r)) return true;
} catch (_) { }
try {
if (typeof sheet.isRowHiddenByFilter === 'function' && sheet.isRowHiddenByFilter(r)) return true;
} catch (_) { }
try {
if (typeof sheet.isRowHidden === 'function' && sheet.isRowHidden(r)) return true;
} catch (_) { }
try {
if (typeof sheet.getRowProperty === 'function') {
const p = sheet.getRowProperty(r);
if (p && (p.hidden === true || p.isHidden === true)) return true;
}
} catch (_) { }
return false;
}
function getCellTextAtPosition(sheet, row0, col0) {
if (!sheet || !Number.isInteger(row0) || !Number.isInteger(col0) || row0 < 0 || col0 < 0) return '';
const cell = sheet.getCellDataAtPosition(row0, col0, CELL_READ_OPTIONS);
if (!cell) return '';
const display = normalizeCellText(displayTextOfCell(cell));
if (display) return display;
return normalizeCellText(textOfCell(cell));
}
function isLikelyDataValue(text) {
const s = normalizeCellText(text);
if (!s) return false;
if (/^https?:\/\//i.test(s)) return true;
if (/^[-+]?\d+(?:[.,]\d+)?%?$/.test(s)) return true;
if (/^\d{4}[\/.-]\d{1,2}[\/.-]\d{1,2}(?:\s+\d{1,2}:\d{2}(?::\d{2})?)?$/.test(s)) return true;
if (/^\d{1,2}:\d{2}(?::\d{2})?$/.test(s)) return true;
if (/^(?:[A-Za-z]{0,3}\d{5,}|\d{5,}[A-Za-z]{0,3})$/.test(s)) return true;
if (/^[\d\W_]+$/.test(s) && /\d/.test(s) && s.length >= 4) return true;
return false;
}
function aliDocsHeaderRowPriority(row0) {
if (row0 === 1) return 0; // 第2行优先
if (row0 === 2) return 1; // 其次第3行
if (row0 === 0) return 2; // 再次第1行
return 3 + Math.max(0, row0);
}
function detectAliDocsHeaderRow(sheet) {
const rowCount = sheet.getRowCount?.() || 0;
const colCount = sheet.getColCount?.() || 0;
if (!rowCount || !colCount) return 0;
const cache = ALIDOCS_HEADER_ROW_CACHE.get(sheet);
const now = Date.now();
if (
cache &&
cache.rowCount === rowCount &&
cache.colCount === colCount &&
(now - cache.at) < ALIDOCS_HEADER_CACHE_TTL_MS &&
cache.nonEmpty >= 2
) {
return cache.row;
}
const maxRows = Math.min(rowCount, Math.max(1, ALIDOCS_HEADER_SCAN_MAX_ROWS));
const maxCols = Math.min(colCount, Math.max(1, ALIDOCS_HEADER_SCAN_MAX_COLS));
let best = null;
for (let r0 = 0; r0 < maxRows; r0 += 1) {
let nonEmpty = 0;
let keywordHits = 0;
let dataLike = 0;
let shortText = 0;
let longText = 0;
for (let c0 = 0; c0 < maxCols; c0 += 1) {
const text = getCellTextAtPosition(sheet, r0, c0);
if (!text) continue;
nonEmpty += 1;
if (text.length <= 24) shortText += 1;
if (text.length >= 40) longText += 1;
if (isLikelyDataValue(text)) dataLike += 1;
const compact = text.replace(/\s+/g, '').toLowerCase();
if (ALIDOCS_HEADER_KEYWORDS.some((kw) => compact.includes(String(kw).toLowerCase()))) {
keywordHits += 1;
}
}
let score = nonEmpty * 1.4 + keywordHits * 2.6 + shortText * 0.9 - dataLike * 1.3 - longText * 0.35;
if (nonEmpty <= 1) score -= 2.5;
const item = { row: r0, score, nonEmpty, keywordHits };
if (!best) {
best = item;
continue;
}
if (item.score > best.score + 0.0001) {
best = item;
continue;
}
if (Math.abs(item.score - best.score) <= 0.0001) {
if (item.keywordHits > best.keywordHits) {
best = item;
continue;
}
if (item.keywordHits === best.keywordHits && aliDocsHeaderRowPriority(item.row) < aliDocsHeaderRowPriority(best.row)) {
best = item;
}
}
}
const fallbackRow = rowCount > 1 ? 1 : 0;
const bestRow = Number.isInteger(best?.row) ? best.row : fallbackRow;
const safeRow = Math.max(0, Math.min(rowCount - 1, bestRow));
ALIDOCS_HEADER_ROW_CACHE.set(sheet, {
row: safeRow,
rowCount,
colCount,
nonEmpty: Number(best?.nonEmpty || 0),
at: now
});
return safeRow;
}
function getDataStartRow(sheet) {
const rowCount = sheet.getRowCount?.() || 0;
if (rowCount <= 1) return 0;
if (isAliDocsRuntime(null, sheet)) {
const headerRow = detectAliDocsHeaderRow(sheet);
return Math.max(1, Math.min(rowCount - 1, headerRow + 1));
}
return rowCount > 2 ? 2 : 1;
}
function getHeaderText(sheet, col) {
// 腾讯默认优先第1行;钉钉自动识别表头行(第2/3行优先)。
const rowCount = sheet.getRowCount?.() || 0;
if (!rowCount) return '';
const isAliDocsSheet = isAliDocsRuntime(null, sheet);
let rowCandidates = [];
if (isAliDocsSheet) {
const preferredRow = detectAliDocsHeaderRow(sheet);
rowCandidates = [
preferredRow,
preferredRow - 1,
preferredRow + 1,
1,
2,
0,
3,
4,
5
];
} else {
rowCandidates = rowCount > 1 ? [0, 1] : [0];
}
const seen = new Set();
for (const r of rowCandidates) {
if (!Number.isInteger(r) || r < 0 || r >= rowCount || seen.has(r)) continue;
seen.add(r);
const t = getCellTextAtPosition(sheet, r, col);
if (t) return t;
}
return '';
}
function getTrackedColumns(sheet, selectedCols = DEFAULT_TARGET_COLS) {
const colCount = sheet.getColCount?.() || 0;
const out = [];
const seen = new Set();
for (const c of Array.from(selectedCols || [])) {
if (!Number.isInteger(c) || c < 0 || c >= colCount) continue;
if (seen.has(c)) continue;
seen.add(c);
out.push(c);
}
return out;
}
function isAliDocsRuntime(app, sheet) {
if (app?.__tmProvider === 'alidocs-controller' || sheet?.__tmProvider === 'alidocs-controller') return true;
return location.hostname === 'alidocs.dingtalk.com';
}
function getDefaultTargetColsByRuntime(app, sheet) {
return isAliDocsRuntime(app, sheet) ? DINGTALK_DEFAULT_TARGET_COLS : DEFAULT_TARGET_COLS;
}
function detectHiddenColumns(sheet) {
const colCount = sheet.getColCount?.() || 0;
if (!colCount) return [];
const out = [];
for (let c = 0; c < colCount; c += 1) {
if (!isColHidden(sheet, c)) continue;
out.push(c);
}
return out;
}
function filterColumnsWithContent(sheet, cols, options = {}) {
const normalized = getTrackedColumns(sheet, cols);
if (!normalized.length) return [];
const rowCount = sheet.getRowCount?.() || 0;
if (rowCount <= 1) return normalized.slice();
const maxScanRows = Number(options?.maxScanRows);
const maxScanCells = Number(options?.maxScanCells);
const endByRows = Math.min(
rowCount,
Number.isFinite(maxScanRows) ? Math.max(3, Math.trunc(maxScanRows)) : rowCount
);
const rowBudget = Math.max(
1,
Math.ceil((Number.isFinite(maxScanCells) ? maxScanCells : Infinity) / Math.max(1, normalized.length))
);
const startRow = Math.max(0, Math.min(rowCount - 1, getDataStartRow(sheet)));
const endRow = Math.min(endByRows, startRow + rowBudget);
const out = [];
for (const c of normalized) {
let hasContent = false;
for (let r0 = startRow; r0 < endRow; r0 += 1) {
const cell = sheet.getCellDataAtPosition(r0, c, CELL_READ_OPTIONS);
if (!cell) continue;
const value = normalizeCellText(textOfCell(cell));
const display = normalizeCellText(displayTextOfCell(cell));
const urls = extractCellUrls(cell);
if (urls.length || value || display) {
hasContent = true;
break;
}
}
if (hasContent) out.push(c);
}
return out;
}
function hasAnyContentInRow(sheet, cols, row0) {
const rowCount = sheet.getRowCount?.() || 0;
if (!rowCount) return false;
const row = Math.max(0, Math.min(rowCount - 1, Math.trunc(Number(row0) || 0)));
const tracked = getTrackedColumns(sheet, cols);
for (const c of tracked) {
const cell = sheet.getCellDataAtPosition(row, c, CELL_READ_OPTIONS);
if (!cell) continue;
if (extractCellUrls(cell).length) return true;
if (normalizeCellText(textOfCell(cell))) return true;
if (normalizeCellText(displayTextOfCell(cell))) return true;
}
return false;
}
function detectMaskedColumns(sheet, options = {}) {
const maxScanRows = Number(options?.maxScanRows);
const maxScanCells = Number(options?.maxScanCells);
const includeFallbackSignals = options?.includeFallbackSignals !== false;
const colCount = sheet.getColCount?.() || 0;
const rowCount = sheet.getRowCount?.() || 0;
const startRow = Math.max(0, Math.min(rowCount - 1, getDataStartRow(sheet)));
if (!colCount || rowCount <= startRow) return [];
const endByRows = Math.min(rowCount, Number.isFinite(maxScanRows) ? Math.max(3, Math.trunc(maxScanRows)) : rowCount);
const rowBudget = Math.max(1, Math.ceil((Number.isFinite(maxScanCells) ? maxScanCells : Infinity) / Math.max(1, colCount)));
const endRow = Math.min(endByRows, startRow + rowBudget);
const starCols = new Set();
const fallbackCols = new Set();
for (let r0 = startRow; r0 < endRow; r0 += 1) {
for (let c0 = 0; c0 < colCount; c0 += 1) {
if (starCols.has(c0)) continue;
const cell = sheet.getCellDataAtPosition(r0, c0, CELL_READ_OPTIONS);
const displayText = normalizeCellText(displayTextOfCell(cell));
const revealText = normalizeCellText(textOfCell(cell));
const hasStar = /^[**]+$/.test(displayText);
const hasMaskedDisplay = looksMaskedDisplayText(displayText);
const hasMaskedReveal = hasMaskedDisplay && revealText && revealText !== displayText;
if (hasStar || hasMaskedDisplay) {
starCols.add(c0);
continue;
}
if (hasMaskedReveal) {
starCols.add(c0);
continue;
}
if (!includeFallbackSignals || fallbackCols.has(c0)) continue;
// 找不到“*列”时的兜底:通过截图/隐藏对象线索识别可能列
const urls = extractCellUrls(cell);
const ext = String(cell?.extendedValue || '');
const hasDrive = urls.some((u) => /^drive:\/\//i.test(u));
const hasHiddenImage = /docimg\d*\.docs\.qq\.com\/image\/|drive:\/\/|drive\.docs\.qq\.com/i.test(ext);
const hasUrlButMaskedText = urls.length > 0 && revealText === '';
if (hasDrive || hasHiddenImage || hasUrlButMaskedText) fallbackCols.add(c0);
}
}
return [...(starCols.size ? starCols : fallbackCols)].sort((a, b) => a - b);
}
function detectAppealBlockColumns(sheet) {
const colCount = sheet.getColCount?.() || 0;
const normHeader = (col) => getHeaderText(sheet, col).replace(/\s+/g, '');
let start = -1;
for (let c = 0; c < colCount; c += 1) {
if (normHeader(c).includes('申诉或调整反馈')) {
start = c;
break;
}
}
if (start < 0) return [];
const out = [start];
const wanted = ['截图1', '截图2', '截图3', '截图', '申诉结果', '责任业务'];
for (let c = start + 1; c < Math.min(colCount, start + 12); c += 1) {
const h = normHeader(c);
if (!h) continue;
if (wanted.some((k) => h.includes(k))) out.push(c);
if (h.includes('责任业务')) break;
}
return [...new Set(out)].sort((a, b) => a - b);
}
function textVisualWidth(text) {
const s = String(text || '');
let w = 0;
for (const ch of s) {
w += /[\u0000-\u00ff]/.test(ch) ? 1 : 2;
}
return w;
}
function estimatePanelMetrics(rows) {
let maxColWidth = textVisualWidth('列');
for (const item of rows || []) {
maxColWidth = Math.max(maxColWidth, textVisualWidth(String(item.colLabel || item.col || '')));
}
const colPx = Math.min(120, Math.max(46, 16 + maxColWidth * 9));
// “值”列宽度与图片预览宽度保持一致
const valuePx = PREVIEW_IMAGE_WIDTH;
const panelPx = Math.max(280, Math.min(window.innerWidth - 24, colPx + valuePx + 36));
return { colPx, valuePx, panelPx };
}
function getSelectionRect() {
const selectors = ['.single-selection', '.select-selection-border'];
for (const sel of selectors) {
const list = document.querySelectorAll(sel);
for (const el of list) {
const rect = el.getBoundingClientRect();
if (rect.width <= 1 || rect.height <= 1) continue;
if (rect.bottom < 0 || rect.top > window.innerHeight) continue;
if (rect.right < 0 || rect.left > window.innerWidth) continue;
return rect;
}
}
return null;
}
function placePanelNearSelection(panel) {
if (!panel || panel.classList.contains('hidden')) return false;
const selectionRect = getSelectionRect();
if (!selectionRect) {
panel.style.left = '';
panel.style.top = '';
panel.style.right = '16px';
panel.style.bottom = '16px';
return false;
}
const panelRect = panel.getBoundingClientRect();
const margin = 8;
const viewportW = window.innerWidth;
const viewportH = window.innerHeight;
const maxLeft = Math.max(margin, viewportW - panelRect.width - margin);
const maxTop = Math.max(margin, viewportH - panelRect.height - margin);
// 默认放在当前格右方
const rightLeft = selectionRect.right + margin;
const rightTop = Math.min(Math.max(margin, selectionRect.top), maxTop);
const canPlaceRight = rightLeft + panelRect.width <= viewportW - margin;
let left = 0;
let top = 0;
if (canPlaceRight) {
left = Math.min(Math.max(margin, rightLeft), maxLeft);
top = rightTop;
} else {
// 右侧放不下时,自动改到当前格上方
left = Math.min(Math.max(margin, selectionRect.left), maxLeft);
const aboveTop = selectionRect.top - panelRect.height - margin;
top = aboveTop >= margin ? aboveTop : margin;
}
panel.style.left = `${Math.round(left)}px`;
panel.style.top = `${Math.round(top)}px`;
panel.style.right = 'auto';
panel.style.bottom = 'auto';
return true;
}
function closeImageViewer(viewer) {
if (!viewer) return;
const img = viewer.querySelector('.tm-img-viewer-img');
if (img) img.removeAttribute('src');
const link = viewer.querySelector('.tm-img-viewer-link');
if (link) {
link.setAttribute('href', '');
link.classList.add('hidden');
}
viewer.classList.add('hidden');
if (Object.prototype.hasOwnProperty.call(viewer.dataset, 'prevOverflow')) {
document.documentElement.style.overflow = viewer.dataset.prevOverflow || '';
delete viewer.dataset.prevOverflow;
}
}
function ensureImageViewer() {
const existing = document.getElementById(IMAGE_VIEWER_ID);
if (existing) return existing;
const viewer = document.createElement('div');
viewer.id = IMAGE_VIEWER_ID;
viewer.className = 'hidden';
viewer.innerHTML = `
`;
viewer.addEventListener('click', (e) => {
const target = e.target;
if (!(target instanceof Element)) return;
if (target.dataset.close === '1') closeImageViewer(viewer);
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !viewer.classList.contains('hidden')) {
closeImageViewer(viewer);
}
});
document.body.appendChild(viewer);
return viewer;
}
function openImageViewer(src) {
const url = String(src || '').trim();
if (!url) return;
const viewer = ensureImageViewer();
const img = viewer.querySelector('.tm-img-viewer-img');
const link = viewer.querySelector('.tm-img-viewer-link');
if (!img) return;
if (viewer.classList.contains('hidden')) {
viewer.dataset.prevOverflow = document.documentElement.style.overflow || '';
document.documentElement.style.overflow = 'hidden';
}
img.setAttribute('src', url);
if (link) {
link.setAttribute('href', url);
link.classList.remove('hidden');
}
viewer.classList.remove('hidden');
}
function createPanel() {
if (document.getElementById(PANEL_ID)) return document.getElementById(PANEL_ID);
if (!document.getElementById(STYLE_ID)) {
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = `
#${PANEL_ID} {
position: fixed;
right: 16px;
bottom: 16px;
z-index: 999999;
width: 360px;
max-height: 60vh;
background: #ffffff;
color: #0f172a;
border: 1px solid #d9e2ec;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.12);
font: 12px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
display: flex;
flex-direction: column;
overflow: hidden;
}
#${PANEL_ID}.hidden { display: none; }
#${PANEL_ID} .hd {
flex: 0 0 auto;
position: relative;
padding: 10px 34px 10px 12px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
}
#${PANEL_ID} .title { font-weight: 600; }
#${PANEL_ID} .panel-close-btn {
position: absolute;
top: 8px;
right: 8px;
width: 22px;
height: 22px;
line-height: 20px;
padding: 0;
border-radius: 11px;
font-size: 16px;
font-weight: 700;
text-align: center;
}
#${PANEL_ID} .ctrl { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; align-items: center; }
#${PANEL_ID} input[type="number"] {
width: 84px; padding: 4px 6px; border-radius: 6px;
border: 1px solid #cbd5e1; background: #ffffff; color: #0f172a;
}
#${PANEL_ID} button {
padding: 4px 8px; border-radius: 6px; border: 1px solid #cbd5e1;
background: #ffffff; color: #0f172a; cursor: pointer;
}
#${PANEL_ID} button:hover { background: #f1f5f9; }
#${PANEL_ID} .bd {
flex: 1 1 auto;
min-height: 0;
min-width: 0;
padding: 8px 10px;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
scrollbar-width: none;
-ms-overflow-style: none;
}
#${PANEL_ID}:hover .bd { scrollbar-width: thin; }
#${PANEL_ID} .bd::-webkit-scrollbar { width: 0; height: 0; }
#${PANEL_ID}:hover .bd::-webkit-scrollbar { width: 8px; height: 0; }
#${PANEL_ID}:hover .bd::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 8px;
}
#${PANEL_ID}:hover .bd::-webkit-scrollbar-track { background: transparent; }
#${PANEL_ID} .tabbar {
display: flex;
gap: 6px;
margin-bottom: 8px;
}
#${PANEL_ID} .tab-btn {
padding: 4px 8px;
}
#${PANEL_ID} .tab-btn.active {
background: #e2e8f0;
border-color: #94a3b8;
font-weight: 600;
}
#${PANEL_ID} .tab-pane { display: none; }
#${PANEL_ID} .tab-pane.active { display: block; }
#${PANEL_ID} .tm-main-table {
min-width: 0;
width: 100%;
max-width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
#${PANEL_ID} .tm-main-table th, #${PANEL_ID} .tm-main-table td { border: 1px solid #e2e8f0; padding: 4px 6px; vertical-align: top; word-break: break-word; }
#${PANEL_ID} .tm-main-table th { background: #f8fafc; font-weight: 600; }
#${PANEL_ID} .tm-main-table th:nth-child(1), #${PANEL_ID} .tm-main-table td:nth-child(1) {
width: var(--tm-col-letter-width, 56px);
max-width: var(--tm-col-letter-width, 56px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: normal;
overflow-wrap: normal;
}
#${PANEL_ID} .tm-main-table th:nth-child(2), #${PANEL_ID} .tm-main-table td:nth-child(2) {
width: var(--tm-col-value-width, 320px);
max-width: var(--tm-col-value-width, 320px);
min-width: 0;
overflow: hidden;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
#${PANEL_ID} .tm-op-note {
margin: 0 0 6px;
color: #64748b;
font-size: 11px;
line-height: 1.35;
min-height: 1.35em;
}
#${PANEL_ID} #tm-tab-ops > table {
width: 100%;
max-width: 100%;
table-layout: fixed;
}
#${PANEL_ID} .tm-history-table {
min-width: 0;
width: 100%;
max-width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
#${PANEL_ID} .tm-history-table th, #${PANEL_ID} .tm-history-table td {
border: 1px solid #e2e8f0;
padding: 4px 6px;
vertical-align: top;
word-break: break-word;
overflow-wrap: anywhere;
}
#${PANEL_ID} .tm-history-table th {
background: #f8fafc;
font-weight: 600;
}
#${PANEL_ID} .tm-history-table th:nth-child(1), #${PANEL_ID} .tm-history-table td:nth-child(1) {
width: 19ch;
min-width: 19ch;
white-space: nowrap;
word-break: normal;
overflow-wrap: normal;
font-variant-numeric: tabular-nums;
}
#${PANEL_ID} .tm-history-table th:nth-child(2), #${PANEL_ID} .tm-history-table td:nth-child(2) { white-space: normal; }
#${PANEL_ID} .tm-history-table th:nth-child(3), #${PANEL_ID} .tm-history-table td:nth-child(3) { width: 6ch; white-space: nowrap; }
#${PANEL_ID} .tm-history-table th:nth-child(4), #${PANEL_ID} .tm-history-table td:nth-child(4) { width: 12ch; white-space: normal; }
#${PANEL_ID} .tm-history-table .tm-meta-row td { background: #ffffff; }
#${PANEL_ID} .tm-history-table .tm-author-other {
font-weight: 700;
color: #b42318;
}
#${PANEL_ID} .tm-history-table .tm-detail-row td {
background: #f8fafc;
font-size: 11px;
border-top: none;
}
#${PANEL_ID} .tm-history-table .tm-detail-text {
display: block;
line-height: 1.4;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
}
#${PANEL_ID} .col-picker {
position: relative;
display: inline-flex;
align-items: center;
flex: 0 0 auto;
max-width: 100%;
min-width: 0;
}
#${PANEL_ID} .col-summary-btn {
display: inline-flex;
align-items: center;
gap: 4px;
width: auto;
min-width: 0;
max-width: none;
overflow: hidden;
white-space: nowrap;
}
#${PANEL_ID} .col-summary-btn::after {
content: '▾';
color: #64748b;
font-size: 12px;
transition: transform 0.16s ease;
transform-origin: 50% 50%;
flex: none;
}
#${PANEL_ID} .col-picker.open .col-summary-btn::after { transform: rotate(180deg); }
#${PANEL_ID} .col-pop {
position: fixed;
top: 0;
left: 0;
right: auto;
z-index: 12;
width: 320px;
max-width: calc(100vw - 16px);
background: #ffffff;
border: 1px solid #d9e2ec;
border-radius: 8px;
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.18);
overflow: hidden;
}
#${PANEL_ID} .col-pop[hidden] { display: none; }
#${PANEL_ID} .col-list {
max-height: min(260px, 36vh);
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
scrollbar-width: none;
-ms-overflow-style: none;
display: grid;
grid-template-columns: 1fr;
gap: 4px 8px;
padding: 8px;
border-bottom: 1px solid #e2e8f0;
}
#${PANEL_ID} .col-list:hover { scrollbar-width: thin; }
#${PANEL_ID} .col-list::-webkit-scrollbar { width: 0; height: 0; }
#${PANEL_ID} .col-list:hover::-webkit-scrollbar { width: 8px; height: 0; }
#${PANEL_ID} .col-list:hover::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 8px;
}
#${PANEL_ID} .col-list label {
display: flex;
align-items: center;
gap: 4px;
min-width: 0;
cursor: grab;
padding: 2px 4px;
border-radius: 6px;
}
#${PANEL_ID} .col-list label.dragging { opacity: 0.55; }
#${PANEL_ID} .col-list label.drag-over {
background: #f8fafc;
outline: 1px dashed #94a3b8;
}
#${PANEL_ID} .col-list label.drag-over-before { box-shadow: inset 0 2px 0 #3b82f6; }
#${PANEL_ID} .col-list label.drag-over-after { box-shadow: inset 0 -2px 0 #3b82f6; }
#${PANEL_ID} .col-list .drag-handle {
color: #94a3b8;
font-size: 12px;
line-height: 1;
user-select: none;
flex: none;
}
#${PANEL_ID} .col-list .txt {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#${PANEL_ID} .col-actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 8px;
background: #f8fafc;
}
#${PANEL_ID} .col-actions button { padding: 3px 8px; }
#${PANEL_ID} .col-manual-modal {
position: fixed;
inset: 0;
z-index: 2147483646;
display: flex;
align-items: center;
justify-content: center;
}
#${PANEL_ID} .col-manual-modal[hidden] { display: none; }
#${PANEL_ID} .col-manual-mask {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.45);
}
#${PANEL_ID} .col-manual-card {
position: relative;
z-index: 1;
width: min(420px, calc(100vw - 24px));
box-sizing: border-box;
background: #ffffff;
border: 1px solid #d9e2ec;
border-radius: 10px;
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.3);
padding: 14px;
display: grid;
gap: 10px;
}
#${PANEL_ID} .col-manual-title {
font-size: 14px;
font-weight: 600;
color: #0f172a;
}
#${PANEL_ID} .col-manual-tip {
font-size: 12px;
color: #64748b;
}
#${PANEL_ID} .col-manual-input {
width: 100%;
box-sizing: border-box;
border: 1px solid #cbd5e1;
border-radius: 8px;
padding: 7px 10px;
font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #0f172a;
outline: none;
}
#${PANEL_ID} .col-manual-input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.18);
}
#${PANEL_ID} .col-manual-error {
min-height: 16px;
font-size: 12px;
color: #b42318;
}
#${PANEL_ID} .col-manual-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
#${PANEL_ID} .col-manual-actions button {
min-width: 64px;
padding: 6px 12px;
}
#${PANEL_ID} .tm-reveal-modal {
position: fixed;
inset: 0;
z-index: 2147483646;
display: flex;
align-items: center;
justify-content: center;
}
#${PANEL_ID} .tm-reveal-modal[hidden] { display: none; }
#${PANEL_ID} .tm-reveal-mask {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.62);
}
#${PANEL_ID} .tm-reveal-card {
position: relative;
z-index: 1;
width: min(96vw, 1320px);
max-height: 90vh;
display: grid;
grid-template-rows: auto auto minmax(0, 1fr);
gap: 8px;
padding: 12px;
border-radius: 10px;
border: 1px solid #cbd5e1;
background: #ffffff;
box-shadow: 0 20px 44px rgba(15, 23, 42, 0.35);
}
#${PANEL_ID} .tm-reveal-head {
display: flex;
flex-direction: column;
gap: 2px;
}
#${PANEL_ID} .tm-reveal-title {
font-size: 14px;
font-weight: 600;
color: #0f172a;
}
#${PANEL_ID} .tm-reveal-meta {
font-size: 12px;
color: #475569;
min-height: 16px;
}
#${PANEL_ID} .tm-reveal-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
flex-wrap: wrap;
}
#${PANEL_ID} .tm-reveal-wrap {
overflow: auto;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #ffffff;
}
#${PANEL_ID} .tm-reveal-table {
width: max-content;
min-width: 100%;
border-collapse: collapse;
table-layout: auto;
}
#${PANEL_ID} .tm-reveal-table th,
#${PANEL_ID} .tm-reveal-table td {
border: 1px solid #e2e8f0;
padding: 4px 6px;
vertical-align: top;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
max-width: none;
}
#${PANEL_ID} .tm-reveal-table thead th {
position: sticky;
top: 0;
z-index: 2;
background: #f8fafc;
font-weight: 600;
padding-right: 12px;
user-select: none;
}
#${PANEL_ID} .tm-reveal-table .tm-reveal-head-label {
display: block;
min-height: 18px;
}
#${PANEL_ID} .tm-reveal-table .tm-reveal-sort-btn {
appearance: none;
border: none;
background: transparent;
padding: 0;
margin: 0;
color: inherit;
font: inherit;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
}
#${PANEL_ID} .tm-reveal-table .tm-reveal-sort-indicator {
color: #2563eb;
font-size: 11px;
line-height: 1;
}
#${PANEL_ID} .tm-reveal-table .tm-reveal-resize-handle {
position: absolute;
top: 0;
right: -4px;
width: 8px;
height: 100%;
cursor: col-resize;
z-index: 3;
}
#${PANEL_ID} .tm-reveal-table .tm-reveal-resize-handle::after {
content: '';
position: absolute;
top: 18%;
bottom: 18%;
left: 50%;
width: 1px;
background: #cbd5e1;
transform: translateX(-50%);
}
#${PANEL_ID}.reveal-resizing,
#${PANEL_ID}.reveal-resizing * {
cursor: col-resize !important;
}
#${PANEL_ID} .tm-reveal-table td.flag {
white-space: nowrap;
}
#${PANEL_ID} .tm-reveal-table td.flag .tag {
display: inline-block;
margin-right: 4px;
border-radius: 10px;
padding: 0 6px;
line-height: 18px;
font-size: 11px;
border: 1px solid #cbd5e1;
background: #f8fafc;
color: #334155;
}
#${PANEL_ID} .tm-reveal-table td.flag .tag.hidden {
border-color: #f59e0b;
background: #fffbeb;
color: #92400e;
}
#${PANEL_ID} .tm-reveal-table td.flag .tag.masked {
border-color: #2563eb;
background: #eff6ff;
color: #1e40af;
}
#${PANEL_ID} .tm-reveal-table .tm-reveal-media-link {
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid #d9e2ec;
border-radius: 6px;
overflow: hidden;
text-decoration: none;
background: #ffffff;
}
#${PANEL_ID} .tm-reveal-table .tm-reveal-thumb {
display: block;
width: auto;
height: 24px;
max-height: 24px;
object-fit: contain;
background: #f8fafc;
}
#${PANEL_ID} .tm-reveal-table .tm-reveal-cell-link {
color: #2563eb;
text-decoration: underline;
text-underline-offset: 2px;
word-break: break-all;
}
#${PANEL_ID} .tm-reveal-table .tm-reveal-cell-text {
margin-top: 2px;
color: #334155;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: anywhere;
}
#${PANEL_ID} .tm-preview-wrap {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
gap: 6px;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
}
#${PANEL_ID} .tm-preview-wrap > * { flex: 0 0 auto; }
#${PANEL_ID} .tm-img-link {
display: inline-flex;
flex-direction: row;
align-items: flex-start;
width: auto;
max-width: none;
border: 1px solid #d9e2ec;
border-radius: 6px;
overflow: hidden;
text-decoration: none;
background: #ffffff;
}
#${PANEL_ID} .tm-img-link-btn {
padding: 0;
cursor: zoom-in;
text-align: left;
font: inherit;
color: inherit;
}
#${PANEL_ID} .tm-img-preview {
display: block;
width: auto;
height: 20px;
max-width: none;
max-height: 20px;
object-fit: contain;
background: #f8fafc;
}
#${IMAGE_VIEWER_ID} {
position: fixed;
inset: 0;
z-index: 2147483647;
display: flex;
align-items: center;
justify-content: center;
}
#${IMAGE_VIEWER_ID}.hidden { display: none; }
#${IMAGE_VIEWER_ID} .tm-img-viewer-card {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
max-width: 92vw;
}
#${IMAGE_VIEWER_ID} .tm-img-viewer-mask {
position: absolute;
inset: 0;
background: rgba(15, 23, 42, 0.72);
}
#${IMAGE_VIEWER_ID} .tm-img-viewer-img {
position: relative;
max-width: 92vw;
max-height: 92vh;
object-fit: contain;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 16px 48px rgba(15, 23, 42, 0.35);
}
#${IMAGE_VIEWER_ID} .tm-img-viewer-link {
font-size: 11px;
line-height: 1.2;
color: #e2e8f0;
text-decoration: underline;
text-underline-offset: 2px;
}
#${IMAGE_VIEWER_ID} .tm-img-viewer-link:hover {
color: #ffffff;
}
#${IMAGE_VIEWER_ID} .tm-img-viewer-link.hidden {
display: none;
}
#${IMAGE_VIEWER_ID} .tm-img-viewer-close {
position: fixed;
top: 14px;
right: 14px;
z-index: 1;
width: 32px;
height: 32px;
line-height: 30px;
text-align: center;
border-radius: 16px;
border: 1px solid rgba(226, 232, 240, 0.9);
background: rgba(15, 23, 42, 0.85);
color: #ffffff;
font-size: 20px;
cursor: pointer;
}
#${LAUNCHER_ID} {
position: fixed;
right: 16px;
bottom: 72px;
z-index: 2147483647;
width: 36px;
height: 36px;
border-radius: 18px;
border: 1px solid #cbd5e1;
background: #ffffff;
color: #0f172a;
font: 12px/36px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
text-align: center;
cursor: pointer;
user-select: none;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.12);
}
#${PANEL_ID}:not(.hidden) ~ #${LAUNCHER_ID} { display: none; }
`;
document.head.appendChild(style);
}
const panel = document.createElement('section');
panel.id = PANEL_ID;
panel.innerHTML = `
手动设置显示列
支持格式:A,C,F 或 A:F
`;
document.body.appendChild(panel);
if (!document.getElementById(LAUNCHER_ID)) {
const launcher = document.createElement('div');
launcher.id = LAUNCHER_ID;
launcher.title = '显示/隐藏 隐藏列内容查看器';
launcher.textContent = '显';
launcher.addEventListener('click', () => {
panel.classList.toggle('hidden');
if (!panel.classList.contains('hidden')) {
requestAnimationFrame(() => placePanelNearSelection(panel));
}
});
document.body.appendChild(launcher);
}
return panel;
}
function safeJsonStringify(v) {
try { return JSON.stringify(v, null, 2); } catch (_) { return String(v); }
}
function tryExtractRowFromSelectionPayload(payload, maxRow) {
const seen = new WeakSet();
const weightedRows = [];
function pushRow(v, weight) {
if (typeof v !== 'number') return;
if (!Number.isFinite(v)) return;
const row = Math.trunc(v);
if (row < 0 || row >= maxRow) return;
weightedRows.push({ row, weight });
}
function walk(node, depth) {
if (!node || depth > 8) return;
if (typeof node !== 'object') return;
if (seen.has(node)) return;
seen.add(node);
const keys = Object.keys(node);
// 这些字段更可能是当前选中行。
const strongKeys = ['row', 'rowIndex', 'startRow', 'startRowIndex', 'r'];
for (const k of strongKeys) pushRow(node[k], 4);
// 这些字段通常是范围终点,权重低一些。
const weakKeys = ['endRow', 'endRowIndex'];
for (const k of weakKeys) pushRow(node[k], 2);
for (const k of keys) {
const v = node[k];
if (typeof v === 'number') {
if (/row/i.test(k)) pushRow(v, 1);
} else if (v && typeof v === 'object') {
walk(v, depth + 1);
}
}
}
walk(payload, 0);
if (!weightedRows.length) return null;
const score = new Map();
for (const { row, weight } of weightedRows) {
score.set(row, (score.get(row) || 0) + weight);
}
let bestRow = null;
let bestScore = -1;
for (const [row, s] of score.entries()) {
if (s > bestScore) {
bestScore = s;
bestRow = row;
}
}
return bestRow;
}
function tryExtractColFromSelectionPayload(payload, maxCol) {
if (!Number.isInteger(maxCol) || maxCol <= 0) return null;
const seen = new WeakSet();
const weightedCols = [];
function pushCol(v, weight) {
if (typeof v !== 'number') return;
if (!Number.isFinite(v)) return;
const col = Math.trunc(v);
if (col < 0 || col >= maxCol) return;
weightedCols.push({ col, weight });
}
function walk(node, depth) {
if (!node || depth > 8) return;
if (typeof node !== 'object') return;
if (seen.has(node)) return;
seen.add(node);
const keys = Object.keys(node);
const strongKeys = ['col', 'colIndex', 'startCol', 'startColIndex', 'c'];
for (const k of strongKeys) pushCol(node[k], 4);
const weakKeys = ['endCol', 'endColIndex'];
for (const k of weakKeys) pushCol(node[k], 2);
for (const k of keys) {
const v = node[k];
if (typeof v === 'number') {
if (/col|column/i.test(k)) pushCol(v, 1);
} else if (v && typeof v === 'object') {
walk(v, depth + 1);
}
}
}
walk(payload, 0);
if (!weightedCols.length) return null;
const score = new Map();
for (const { col, weight } of weightedCols) {
score.set(col, (score.get(col) || 0) + weight);
}
let bestCol = null;
let bestScore = -1;
for (const [col, s] of score.entries()) {
if (s > bestScore) {
bestScore = s;
bestCol = col;
}
}
return bestCol;
}
function parseA1Row0(raw, maxRow) {
if (typeof raw !== 'string') return null;
const text = raw.trim();
if (!text || text.length > 40) return null;
// A1 或 A1:B8 这类名称框格式
const m = text.match(/^\$?[A-Za-z]{1,4}\$?(\d{1,7})(?::\$?[A-Za-z]{1,4}\$?(\d{1,7}))?$/);
if (!m) return null;
const row0 = Number(m[1]) - 1;
if (!Number.isFinite(row0) || row0 < 0 || row0 >= maxRow) return null;
return row0;
}
function tryExtractRowFromDom(maxRow) {
const checked = new Set();
const candidates = [];
if (document.activeElement) candidates.push(document.activeElement);
candidates.push(...document.querySelectorAll('input, [role="textbox"], textarea'));
for (const el of candidates) {
if (!el || checked.has(el)) continue;
checked.add(el);
const value =
(typeof el.value === 'string' ? el.value : '') ||
(typeof el.textContent === 'string' ? el.textContent : '');
const row0 = parseA1Row0(value, maxRow);
if (typeof row0 === 'number') return row0;
}
return null;
}
function tryExtractRowFromView(app, maxRow) {
const pickRow = (v) => {
if (typeof v !== 'number' || !Number.isFinite(v)) return null;
const row = Math.trunc(v);
if (row < 0 || row >= maxRow) return null;
return row;
};
try {
const row = pickRow(app?.view?.getActiveCell?.()?.rowIndex);
if (typeof row === 'number') return row;
} catch (_) { }
try {
const sel = app?.view?.getSelection?.();
const row =
pickRow(sel?.activeCell?.rowIndex) ??
pickRow(sel?.rangeSelections?.[0]?.startRowIndex) ??
pickRow(sel?.rangeSelections?.[0]?.rowIndex);
if (typeof row === 'number') return row;
} catch (_) { }
try {
const ranges = app?.view?.getSelectionRanges?.();
const row =
pickRow(ranges?.[0]?.startRowIndex) ??
pickRow(ranges?.[0]?.rowIndex);
if (typeof row === 'number') return row;
} catch (_) { }
return null;
}
function tryExtractColFromView(app, maxCol) {
const pickCol = (v) => {
if (typeof v !== 'number' || !Number.isFinite(v)) return null;
const col = Math.trunc(v);
if (col < 0 || col >= maxCol) return null;
return col;
};
try {
const col = pickCol(app?.view?.getActiveCell?.()?.colIndex);
if (typeof col === 'number') return col;
} catch (_) { }
try {
const sel = app?.view?.getSelection?.();
const col =
pickCol(sel?.activeCell?.colIndex) ??
pickCol(sel?.rangeSelections?.[0]?.startColIndex) ??
pickCol(sel?.rangeSelections?.[0]?.colIndex);
if (typeof col === 'number') return col;
} catch (_) { }
try {
const ranges = app?.view?.getSelectionRanges?.();
const col =
pickCol(ranges?.[0]?.startColIndex) ??
pickCol(ranges?.[0]?.colIndex);
if (typeof col === 'number') return col;
} catch (_) { }
return null;
}
function install() {
const panel = createPanel();
panel.classList.add('hidden');
ensureImageViewer();
const panelBody = panel.querySelector('.bd');
const rowInputBoot = panel.querySelector('#tm-row-input');
const btnRefreshBoot = panel.querySelector('#tm-refresh');
const btnRevealBoot = panel.querySelector('#tm-reveal-all');
const btnToggleBoot = panel.querySelector('#tm-toggle');
const btnDetectBoot = panel.querySelector('#tm-col-detect');
const btnColManualBoot = panel.querySelector('#tm-col-manual');
const btnColAllBoot = panel.querySelector('#tm-col-all');
const btnColClearBoot = panel.querySelector('#tm-col-clear');
let initialized = false;
let doRefresh = () => {
tryInit();
};
let doRevealAll = () => {
tryInit();
};
let doDetectCols = () => {
tryInit();
};
let doManualSelectCols = () => { };
let doSelectAllCols = () => { };
let doClearCols = () => { };
let doToggle = () => {
panel.classList.add('hidden');
};
let doReposition = () => {
placePanelNearSelection(panel);
};
let repositionRaf = 0;
const scheduleReposition = () => {
if (panel.classList.contains('hidden')) return;
if (repositionRaf) return;
repositionRaf = requestAnimationFrame(() => {
repositionRaf = 0;
doReposition();
});
};
btnRefreshBoot?.addEventListener('click', () => doRefresh());
btnRevealBoot?.addEventListener('click', () => doRevealAll());
btnToggleBoot?.addEventListener('click', () => doToggle());
btnDetectBoot?.addEventListener('click', () => doDetectCols());
btnColManualBoot?.addEventListener('click', () => doManualSelectCols());
btnColAllBoot?.addEventListener('click', () => doSelectAllCols());
btnColClearBoot?.addEventListener('click', () => doClearCols());
rowInputBoot?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') doRefresh();
});
window.addEventListener('resize', () => scheduleReposition(), { passive: true });
// 避免面板内部滚动触发全局重定位,导致滚动偶发失效
window.addEventListener('scroll', () => scheduleReposition(), { passive: true });
if (typeof ResizeObserver === 'function') {
const resizeObserver = new ResizeObserver(() => scheduleReposition());
resizeObserver.observe(panel);
if (panelBody) resizeObserver.observe(panelBody);
}
panel.addEventListener('wheel', (e) => {
if (panel.classList.contains('hidden')) return;
const target = e.target;
if (!(target instanceof Element)) return;
const bdEl = panel.querySelector('.bd');
const listEl = panel.querySelector('#tm-col-list');
if (!bdEl) return;
let scrollEl = null;
if (listEl && listEl.contains(target) && listEl.scrollHeight > listEl.clientHeight + 1) {
scrollEl = listEl;
} else {
scrollEl = bdEl;
}
const max = scrollEl.scrollHeight - scrollEl.clientHeight;
if (max <= 0) return;
const delta = Number(e.deltaY) || 0;
if (delta === 0) return;
const atTop = scrollEl.scrollTop <= 0;
const atBottom = scrollEl.scrollTop >= max - 1;
const shouldHandle = (delta < 0 && !atTop) || (delta > 0 && !atBottom);
if (!shouldHandle) return;
scrollEl.scrollTop += delta;
e.preventDefault();
e.stopPropagation();
}, { capture: true, passive: false });
panel.addEventListener('click', (e) => {
const target = e.target;
if (!(target instanceof Element)) return;
const trigger = target.closest('.tm-img-link[data-full-src]');
if (!trigger || !panel.contains(trigger)) return;
const src = String(trigger.getAttribute('data-full-src') || '').trim();
if (!src) return;
e.preventDefault();
openImageViewer(src);
});
const initDataLayer = ({ app, sheet }) => {
if (initialized) return;
initialized = true;
const rowInput = panel.querySelector('#tm-row-input');
const tbody = panel.querySelector('#tm-tbody');
const opTbody = panel.querySelector('#tm-op-tbody');
const opNote = panel.querySelector('#tm-op-note');
const colPicker = panel.querySelector('#tm-col-picker');
const colPop = panel.querySelector('#tm-col-pop');
const colList = panel.querySelector('#tm-col-list');
const colSummary = panel.querySelector('#tm-col-summary');
const btnDetect = panel.querySelector('#tm-col-detect');
const colManualModal = panel.querySelector('#tm-col-manual-modal');
const colManualInput = panel.querySelector('#tm-col-manual-input');
const colManualError = panel.querySelector('#tm-col-manual-error');
const colManualCancel = panel.querySelector('#tm-col-manual-cancel');
const colManualOk = panel.querySelector('#tm-col-manual-ok');
const btnReveal = panel.querySelector('#tm-reveal-all');
const revealModal = panel.querySelector('#tm-reveal-modal');
const revealMeta = panel.querySelector('#tm-reveal-meta');
const revealTable = panel.querySelector('#tm-reveal-table');
const revealWrap = panel.querySelector('#tm-reveal-wrap');
const revealClose = panel.querySelector('#tm-reveal-close');
const revealCopy = panel.querySelector('#tm-reveal-copy');
const tabColsBtn = panel.querySelector('#tm-tab-cols-btn');
const tabOpsBtn = panel.querySelector('#tm-tab-ops-btn');
const tabCols = panel.querySelector('#tm-tab-cols');
const tabOps = panel.querySelector('#tm-tab-ops');
let currentRow1 = 2;
let pendingOpsRow0 = 1;
let candidateCols = [];
let selectedCols = new Set();
let draggingCol = null;
let activeTab = 'cols';
let opRenderSeq = 0;
let valueRenderSeq = 0;
let selectionRenderTimer = 0;
let pendingSelectionRow1 = null;
let pendingSelectionCol0 = null;
let refreshOpsForCurrentRow = () => { };
let colHeaderRetryTimer = 0;
let tryScheduleColHeaderRetry = () => { };
let revealLastTSV = '';
let revealSeq = 0;
let revealRenderState = null;
let revealSortOrder = 'asc';
let revealColWidths = {};
let revealResizeSession = null;
const colOrderStorageKey = getColumnOrderStorageKey(app);
const revealViewStorageKey = getRevealViewStorageKey(app);
const revealSavedConfig = loadRevealViewConfig(revealViewStorageKey);
revealSortOrder = normalizeRevealSortOrder(revealSavedConfig.sortOrder);
revealColWidths = revealSavedConfig.colWidths || {};
panel.style.setProperty('--tm-preview-width', `${PREVIEW_IMAGE_WIDTH}px`);
const maxRowForInit = sheet.getRowCount?.() || 1;
const initialRow0 =
tryExtractRowFromView(app, maxRowForInit) ??
tryExtractRowFromDom(maxRowForInit);
if (typeof initialRow0 === 'number') currentRow1 = initialRow0 + 1;
const switchTab = (tab) => {
activeTab = tab === 'ops' ? 'ops' : 'cols';
if (tabColsBtn) tabColsBtn.classList.toggle('active', activeTab === 'cols');
if (tabOpsBtn) tabOpsBtn.classList.toggle('active', activeTab === 'ops');
if (tabCols) tabCols.classList.toggle('active', activeTab === 'cols');
if (tabOps) tabOps.classList.toggle('active', activeTab === 'ops');
if (activeTab === 'ops') refreshOpsForCurrentRow();
};
tabColsBtn?.addEventListener('click', () => switchTab('cols'));
tabOpsBtn?.addEventListener('click', () => switchTab('ops'));
switchTab('cols');
const resolveInitialRow1 = async () => {
const started = Date.now();
while (Date.now() - started < 2200) {
const row0 =
tryExtractRowFromView(app, maxRowForInit) ??
tryExtractRowFromDom(maxRowForInit);
if (typeof row0 === 'number') return row0 + 1;
await new Promise((r) => setTimeout(r, 120));
}
return currentRow1;
};
const placeColPickerPop = () => {
if (!colPop || colPop.hidden || !colSummary) return;
const margin = 8;
const anchor = colSummary.getBoundingClientRect();
const rect = colPop.getBoundingClientRect();
const popW = Math.min(rect.width || 320, Math.max(120, window.innerWidth - margin * 2));
let left = anchor.left;
if (left + popW > window.innerWidth - margin) left = window.innerWidth - margin - popW;
if (left < margin) left = margin;
let top = anchor.bottom + 6;
const popH = rect.height || 260;
if (top + popH > window.innerHeight - margin) {
top = Math.max(margin, anchor.top - popH - 6);
}
colPop.style.left = `${Math.round(left)}px`;
colPop.style.top = `${Math.round(top)}px`;
};
const setColPickerOpen = (open) => {
const shouldOpen = !!open;
if (colPicker) colPicker.classList.toggle('open', shouldOpen);
if (colPop) colPop.hidden = !shouldOpen;
if (colSummary) colSummary.setAttribute('aria-expanded', shouldOpen ? 'true' : 'false');
if (shouldOpen) {
requestAnimationFrame(placeColPickerPop);
tryScheduleColHeaderRetry();
}
};
const toggleColPicker = () => {
const isOpen = colPicker?.classList.contains('open');
setColPickerOpen(!isOpen);
};
colSummary?.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
toggleColPicker();
});
document.addEventListener('mousedown', (e) => {
const target = e.target;
if (!(target instanceof Node)) return;
if (!colPicker?.contains(target)) setColPickerOpen(false);
});
window.addEventListener('resize', () => {
if (colPicker?.classList.contains('open')) placeColPickerPop();
}, { passive: true });
window.addEventListener('scroll', () => {
if (colPicker?.classList.contains('open')) placeColPickerPop();
}, { passive: true });
const isManualDialogOpen = () => !!colManualModal && !colManualModal.hidden;
const setManualDialogOpen = (open, preset = '') => {
if (!colManualModal) return;
const shouldOpen = !!open;
colManualModal.hidden = !shouldOpen;
if (!shouldOpen) return;
if (colManualError) colManualError.textContent = '';
if (!colManualInput) return;
colManualInput.value = String(preset || '');
requestAnimationFrame(() => {
colManualInput.focus();
colManualInput.select();
});
};
const isRevealDialogOpen = () => !!revealModal && !revealModal.hidden;
const setRevealDialogOpen = (open) => {
if (!revealModal) return;
const shouldOpen = !!open;
if (!shouldOpen) {
revealSeq += 1;
stopRevealResize(false);
}
revealModal.hidden = !shouldOpen;
};
const compactCellText = (text) => normalizeCellText(text || '');
const tsvCell = (text) => String(text == null ? '' : text).replace(/\t/g, ' ').replace(/\r?\n/g, ' ');
const persistRevealView = () => {
saveRevealViewConfig(revealViewStorageKey, {
sortOrder: revealSortOrder,
colWidths: revealColWidths
});
};
const getRevealDefaultColWidth = (key) => {
if (key === REVEAL_ROW_COL_KEY) return REVEAL_DEFAULT_ROW_COL_WIDTH;
return REVEAL_DEFAULT_DATA_COL_WIDTH;
};
const getRevealColWidth = (key) => {
const stored = normalizeRevealColWidth(revealColWidths?.[key]);
if (stored != null) return stored;
return getRevealDefaultColWidth(key);
};
const getRevealColStyle = (key) => {
const width = getRevealColWidth(key);
return `width:${width}px;min-width:${width}px;max-width:${width}px;`;
};
const updateRevealColWidthInDom = (key, width) => {
if (!revealTable) return;
const normalized = normalizeRevealColWidth(width);
if (normalized == null) return;
const cols = revealTable.querySelectorAll('col[data-reveal-key]');
for (const col of cols) {
if (!(col instanceof HTMLElement)) continue;
if (String(col.dataset.revealKey || '') !== key) continue;
col.style.width = `${normalized}px`;
col.style.minWidth = `${normalized}px`;
col.style.maxWidth = `${normalized}px`;
}
};
const getSortedRevealRows = (rows) => {
const list = Array.isArray(rows) ? rows.slice() : [];
list.sort((a, b) => {
const diff = (Number(a?.row1) || 0) - (Number(b?.row1) || 0);
if (diff === 0) return 0;
return revealSortOrder === 'desc' ? -diff : diff;
});
return list;
};
const renderRevealCellHtml = (cellData) => {
const item = cellData || {};
const text = compactCellText(item.text || '');
const urls = Array.isArray(item.urls) ? item.urls : [];
const imageUrls = Array.isArray(item.imageUrls) ? item.imageUrls : [];
const imageSet = new Set(imageUrls);
const linkUrls = urls.filter((u) => !imageSet.has(u));
let html = '';
if (imageUrls.length) {
const thumbs = imageUrls.map((u) => {
const safe = escapeHtml(u);
return ``;
}).join('');
html += `${thumbs}
`;
}
if (linkUrls.length) {
html += linkUrls.map((u) => {
const safe = escapeHtml(u);
return `${safe}`;
}).join('
');
}
const shouldShowText = text && text !== urls.join(' | ');
if (shouldShowText) html += `${escapeHtml(text)}
`;
if (html) return html;
return text ? escapeHtml(text) : '';
};
const renderRevealTableFromState = (options = {}) => {
if (!revealTable || !revealRenderState) return;
const keepScroll = options?.keepScroll === true;
const prevScrollTop = keepScroll ? (revealWrap?.scrollTop || 0) : 0;
const prevScrollLeft = keepScroll ? (revealWrap?.scrollLeft || 0) : 0;
const colDefs = [
{ key: REVEAL_ROW_COL_KEY, label: '行号' },
...(Array.isArray(revealRenderState.colDefs) ? revealRenderState.colDefs : [])
];
const rows = getSortedRevealRows(revealRenderState.rows);
const arrow = revealSortOrder === 'desc' ? '↓' : '↑';
const colgroupHtml = `${colDefs
.map((col) => ``)
.join('')}`;
const theadHtml = `${colDefs.map((col) => {
if (col.key === REVEAL_ROW_COL_KEY) {
return ` | `;
}
return `${escapeHtml(col.label)} | `;
}).join('')}
`;
const tbodyHtml = `${rows.map((row) => {
const cells = (Array.isArray(row.values) ? row.values : [])
.map((v) => `${renderRevealCellHtml(v)} | `)
.join('');
return `| ${row.row1} | ${cells}
`;
}).join('')}`;
revealTable.innerHTML = `${colgroupHtml}${theadHtml}${tbodyHtml}`;
if (keepScroll && revealWrap) {
revealWrap.scrollTop = prevScrollTop;
revealWrap.scrollLeft = prevScrollLeft;
}
const tsvHeader = colDefs.map((col) => col.label).join('\t');
const tsvRows = rows.map((row) => {
return [row.row1, ...(Array.isArray(row.values) ? row.values.map((v) => v?.exportText || '') : [])]
.map(tsvCell)
.join('\t');
});
revealLastTSV = [tsvHeader, ...tsvRows].join('\n');
};
function stopRevealResize(shouldPersist = true) {
if (!revealResizeSession) return;
document.documentElement.style.userSelect = revealResizeSession.prevUserSelect || '';
panel.classList.remove('reveal-resizing');
revealResizeSession = null;
if (shouldPersist) persistRevealView();
}
const onRevealResizeMove = (e) => {
if (!revealResizeSession) return;
const delta = Number(e?.clientX || 0) - revealResizeSession.startX;
const nextWidth = normalizeRevealColWidth(revealResizeSession.startWidth + delta);
if (nextWidth == null) return;
revealColWidths[revealResizeSession.key] = nextWidth;
updateRevealColWidthInDom(revealResizeSession.key, nextWidth);
};
const onRevealResizeEnd = () => stopRevealResize(true);
document.addEventListener('mousemove', onRevealResizeMove, true);
document.addEventListener('mouseup', onRevealResizeEnd, true);
const normalizeRevealHeaderKey = (text) => (
String(text == null ? '' : text)
.replace(/\s+/g, '')
.toLowerCase()
);
const revealHeaderBaseKey = (text) => {
const normalized = normalizeRevealHeaderKey(text);
if (!normalized) return '';
return normalized
.replace(/[0-90-9]+/g, '')
.replace(/[一二三四五六七八九十百千万两〇零]+/g, '')
.replace(/[()()【】\[\]{}<>《》「」『』\-_::,.,。·•]/g, '');
};
const buildRevealColumnSet = () => {
const rowCount = sheet.getRowCount?.() || 0;
const colCount = sheet.getColCount?.() || 0;
if (!rowCount || !colCount) {
return {
cols: [],
hiddenSet: new Set(),
maskedSet: new Set(),
fallbackSet: new Set(),
popupSync: false,
truncatedMaskScan: false
};
}
const hiddenCols = detectHiddenColumns(sheet);
const hiddenSet = new Set(hiddenCols);
const totalCells = Math.max(1, rowCount * Math.max(1, colCount));
const maxMaskCells = Math.min(3_000_000, Math.max(totalCells, DEEP_SCAN_MAX_CELLS));
const maskedCols = detectMaskedColumns(sheet, {
maxScanRows: rowCount,
maxScanCells: maxMaskCells,
includeFallbackSignals: true
});
const maskedSet = new Set(maskedCols);
const popupCols = (() => {
const ordered = [];
const used = new Set();
for (const c of candidateCols) {
if (!selectedCols.has(c)) continue;
if (used.has(c)) continue;
used.add(c);
ordered.push(c);
}
for (const c of selectedCols) {
if (used.has(c)) continue;
used.add(c);
ordered.push(c);
}
return getTrackedColumns(sheet, ordered);
})();
const popupSync = popupCols.length > 0;
// 全表列与面板“显示列”同步;若未同步则按识别结果回退。
let cols = popupSync
? popupCols.slice()
: getTrackedColumns(sheet, maskedCols);
if (!cols.length) {
const runtimeDefaultCols = getTrackedColumns(sheet, getDefaultTargetColsByRuntime(app, sheet));
cols = runtimeDefaultCols.slice();
}
// 先剔除整列无值列,避免弹窗里出现大量空列。
if (cols.length) {
const scanCells = Math.max(DEEP_SCAN_MAX_CELLS, rowCount * Math.max(1, cols.length));
cols = filterColumnsWithContent(sheet, cols, {
maxScanRows: rowCount,
maxScanCells: scanCells
});
}
const fallbackSet = new Set();
// 与面板列同步时,如果某些同类列被选中但无值,自动补全同名且有值的列。
if (popupSync) {
const nonEmptySelectedCols = new Set(cols);
const emptyDetectedCols = popupCols.filter((c) => !nonEmptySelectedCols.has(c));
const fallbackBases = new Set();
for (const c of emptyDetectedCols) {
const base = revealHeaderBaseKey(getHeaderText(sheet, c));
if (base) fallbackBases.add(base);
}
if (fallbackBases.size) {
const baseCandidates = [];
const existingSet = new Set(cols);
for (let c = 0; c < colCount; c += 1) {
if (existingSet.has(c)) continue;
const base = revealHeaderBaseKey(getHeaderText(sheet, c));
if (!base || !fallbackBases.has(base)) continue;
baseCandidates.push(c);
}
if (baseCandidates.length) {
const candidateScanCells = Math.max(DEEP_SCAN_MAX_CELLS, rowCount * Math.max(1, baseCandidates.length));
const contentCandidates = filterColumnsWithContent(sheet, baseCandidates, {
maxScanRows: rowCount,
maxScanCells: candidateScanCells
});
for (const c of contentCandidates) {
if (existingSet.has(c)) continue;
existingSet.add(c);
fallbackSet.add(c);
cols.push(c);
}
}
}
}
const truncatedMaskScan = maxMaskCells < totalCells;
return {
cols,
hiddenSet,
maskedSet,
fallbackSet,
popupSync,
truncatedMaskScan
};
};
const buildRevealTable = async () => {
const seq = ++revealSeq;
const rowCount = sheet.getRowCount?.() || 0;
const colCount = sheet.getColCount?.() || 0;
if (!rowCount || !colCount) {
return { ok: false, message: '当前表格为空,无法生成全表视图' };
}
if (revealMeta) revealMeta.textContent = '正在识别隐藏列与*列...';
const { cols, hiddenSet, maskedSet, fallbackSet, popupSync, truncatedMaskScan } = buildRevealColumnSet();
if (!cols.length) {
return { ok: false, message: '未识别到可显示列(已按弹窗显示列同步)' };
}
const dataStartRow = getDataStartRow(sheet);
const colLabels = cols.map((c) => {
const h = getHeaderText(sheet, c);
const tags = [];
if (hiddenSet.has(c)) tags.push('隐藏');
if (maskedSet.has(c)) tags.push('*');
if (fallbackSet.has(c)) tags.push('补全');
const tag = tags.length ? `[${tags.join('+')}]` : '';
return `${tag}${colToLetter(c)}${h ? ` (${h})` : ''}`;
});
const rows = [];
const totalRows = Math.max(0, rowCount - dataStartRow);
let hiddenRowCount = 0;
let maskedRowCount = 0;
const flushEvery = 40;
const quickCellUrlText = (cell) => {
const ext = String(cell?.extendedValue || '');
if (ext) {
const m = ext.match(/https?:\/\/[^\s"'\\\],]+/i);
if (m && m[0]) return m[0];
}
const vHyper = String(cell?.value?.hyperlink?.url || '').trim();
if (vHyper) return vHyper;
const fHyper = String(cell?.formattedValue?.hyperlink?.url || '').trim();
if (fHyper) return fHyper;
return '';
};
const normalizeUrlText = (url) => {
const raw = String(url == null ? '' : url).trim();
if (!raw) return '';
const trimmed = raw.replace(/[),.;]+$/, '');
if (!/^(https?:\/\/|drive:\/\/)/i.test(trimmed)) return '';
return trimmed;
};
const collectTextUrls = (text) => {
const out = [];
const seen = new Set();
const push = (value) => {
const normalized = normalizeUrlText(value);
if (!normalized || seen.has(normalized)) return;
seen.add(normalized);
out.push(normalized);
};
const source = String(text == null ? '' : text);
if (!source) return out;
const re = /(drive:\/\/[^\s"'\\\],]+|https?:\/\/[^\s"'\\\],]+)/gi;
let m = null;
while ((m = re.exec(source)) !== null) {
if (m[1]) push(m[1]);
}
return out;
};
const collectCellUrls = (cell, revealText, displayText) => {
const out = [];
const seen = new Set();
const push = (value) => {
const normalized = normalizeUrlText(value);
if (!normalized || seen.has(normalized)) return;
seen.add(normalized);
out.push(normalized);
};
for (const u of extractCellUrls(cell)) push(u);
push(quickCellUrlText(cell));
for (const u of collectTextUrls(revealText)) push(u);
for (const u of collectTextUrls(displayText)) push(u);
return out;
};
const buildRevealCell = (cell) => {
const display = compactCellText(displayTextOfCell(cell));
const reveal = compactCellText(textOfCell(cell));
const urls = collectCellUrls(cell, reveal, display);
const imageUrls = urls.filter(isPreviewableImageUrl);
const textSource = reveal ? 'reveal' : (display ? 'display' : 'urls');
const finalText = reveal || display || (urls.length ? urls.join(' | ') : '');
const text = compactCellText(finalText);
const hasDriveUrls = urls.some((u) => /^drive:\/\//i.test(String(u)));
return {
text,
exportText: text || (urls.length ? urls.join(' | ') : ''),
urls,
imageUrls,
isMasked: looksMaskedDisplayText(display) && reveal && reveal !== display,
textSource,
hasDriveUrls
};
};
const resolveRevealCellUrls = async (entry) => {
const item = entry || {};
const urls = Array.isArray(item.urls) ? item.urls : [];
if (!urls.length || !urls.some((u) => /^drive:\/\//i.test(String(u)))) return item;
const resolvedUrls = await resolveUrls(urls);
const nextImageUrls = resolvedUrls.filter(isPreviewableImageUrl);
let nextText = compactCellText(item.text || '');
let nextExportText = compactCellText(item.exportText || '');
if (item.textSource === 'urls') {
nextText = compactCellText(resolvedUrls.join(' | '));
nextExportText = nextText || (resolvedUrls.length ? resolvedUrls.join(' | ') : '');
} else if (!nextExportText && resolvedUrls.length) {
nextExportText = resolvedUrls.join(' | ');
}
return {
...item,
urls: resolvedUrls,
imageUrls: nextImageUrls,
text: nextText,
exportText: nextExportText,
hasDriveUrls: false
};
};
for (let r0 = dataStartRow; r0 < rowCount; r0 += 1) {
if (seq !== revealSeq) return { ok: false, message: '任务已取消,请重试' };
if ((r0 - dataStartRow) % flushEvery === 0) {
if (revealMeta) revealMeta.textContent = `正在扫描第 ${r0 - dataStartRow + 1}/${Math.max(1, totalRows)} 行...`;
await sleep(0);
}
const rowHidden = isRowHidden(sheet, r0);
if (rowHidden) hiddenRowCount += 1;
let rowMasked = false;
let rowHasValue = false;
const values = [];
for (const c of cols) {
const cell = sheet.getCellDataAtPosition(r0, c, CELL_READ_OPTIONS);
let entry = buildRevealCell(cell);
if (entry.hasDriveUrls) {
entry = await resolveRevealCellUrls(entry);
if (seq !== revealSeq) return { ok: false, message: '任务已取消,请重试' };
}
if (!rowMasked && entry.isMasked) rowMasked = true;
if (!rowHasValue && compactCellText(entry.exportText)) rowHasValue = true;
values.push(entry);
}
if (rowMasked) maskedRowCount += 1;
if (!rowHasValue && !rowHidden && !rowMasked) {
continue;
}
rows.push({
row1: r0 + 1,
rowHidden,
rowMasked,
values
});
}
if (seq !== revealSeq) return { ok: false, message: '任务已取消,请重试' };
if (!rows.length) {
return { ok: false, message: '已识别到目标列,但目标行无可显示内容' };
}
revealRenderState = {
colDefs: cols.map((c, idx) => ({
key: `c:${c}`,
label: String(colLabels[idx] || colToLetter(c) || '')
})),
rows
};
renderRevealTableFromState({ keepScroll: false });
const extraNotes = [];
if (truncatedMaskScan) extraNotes.push('*列扫描已按上限截断');
if (popupSync) extraNotes.push('列已与弹窗显示列同步');
if (fallbackSet.size) extraNotes.push(`已补全 ${fallbackSet.size} 个同类有值列`);
const extraNote = extraNotes.length ? `(${extraNotes.join(';')})` : '';
return {
ok: true,
rows: rows.length,
cols: cols.length,
hiddenRows: hiddenRowCount,
maskedRows: maskedRowCount,
meta: `已显示 ${rows.length} 行 / ${cols.length} 列;隐藏行 ${hiddenRowCount},*行 ${maskedRowCount}${extraNote}`
};
};
const getOrderedSelectedCols = () => {
const ordered = [];
const used = new Set();
for (const c of candidateCols) {
if (!selectedCols.has(c)) continue;
if (used.has(c)) continue;
used.add(c);
ordered.push(c);
}
for (const c of selectedCols) {
if (used.has(c)) continue;
used.add(c);
ordered.push(c);
}
return getTrackedColumns(sheet, ordered);
};
const applySavedOrder = (cols) => {
const normalized = getTrackedColumns(sheet, cols);
const saved = loadColumnOrder(colOrderStorageKey);
if (!saved.length) return normalized;
const set = new Set(normalized);
const ordered = [];
for (const c of saved) {
if (!set.has(c)) continue;
ordered.push(c);
set.delete(c);
}
for (const c of normalized) {
if (!set.has(c)) continue;
ordered.push(c);
set.delete(c);
}
return ordered;
};
const persistCandidateOrder = () => {
saveColumnOrder(colOrderStorageKey, candidateCols);
};
const reorderCandidateCols = (fromCol, toCol, placeAfter = false) => {
if (!Number.isInteger(fromCol) || !Number.isInteger(toCol) || fromCol === toCol) return;
const fromIdx = candidateCols.indexOf(fromCol);
const toIdx = candidateCols.indexOf(toCol);
if (fromIdx < 0 || toIdx < 0) return;
const next = candidateCols.slice();
const [moved] = next.splice(fromIdx, 1);
let insertIdx = next.indexOf(toCol);
if (insertIdx < 0) insertIdx = next.length;
else if (placeAfter) insertIdx += 1;
next.splice(insertIdx, 0, moved);
candidateCols = next;
persistCandidateOrder();
};
const updateColSummary = () => {
const letters = getOrderedSelectedCols().map(colToLetter);
colSummary.textContent = '显示列';
colSummary.title = letters.length ? `已选: ${letters.join(',')}` : '显示列';
};
const ensureCandidateCols = (cols) => {
const normalized = getTrackedColumns(sheet, cols);
if (!normalized.length) return;
const set = new Set(candidateCols);
let changed = false;
for (const c of normalized) {
if (set.has(c)) continue;
set.add(c);
candidateCols.push(c);
changed = true;
}
if (!changed) return;
candidateCols = applySavedOrder(candidateCols);
};
const renderColList = () => {
colList.innerHTML = '';
if (!candidateCols.length) {
const empty = document.createElement('div');
empty.textContent = '未识别到可选列';
colList.appendChild(empty);
updateColSummary();
return;
}
const clearDragVisuals = () => {
colList.querySelectorAll('label.dragging, label.drag-over, label.drag-over-before, label.drag-over-after').forEach((el) => {
el.classList.remove('dragging', 'drag-over', 'drag-over-before', 'drag-over-after');
delete el.dataset.dropPos;
});
};
for (const c of candidateCols) {
const label = document.createElement('label');
label.draggable = true;
label.dataset.col = String(c);
label.title = '拖拽调整显示顺序';
label.addEventListener('dragstart', (e) => {
draggingCol = c;
label.classList.add('dragging');
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', String(c));
}
});
label.addEventListener('dragover', (e) => {
if (draggingCol == null || draggingCol === c) return;
e.preventDefault();
const rect = label.getBoundingClientRect();
const placeAfter = e.clientY >= rect.top + rect.height / 2;
clearDragVisuals();
label.classList.add('drag-over');
label.classList.add(placeAfter ? 'drag-over-after' : 'drag-over-before');
label.dataset.dropPos = placeAfter ? 'after' : 'before';
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
});
label.addEventListener('dragleave', () => {
label.classList.remove('drag-over', 'drag-over-before', 'drag-over-after');
delete label.dataset.dropPos;
});
label.addEventListener('drop', (e) => {
e.preventDefault();
const placeAfter = label.dataset.dropPos === 'after';
label.classList.remove('drag-over', 'drag-over-before', 'drag-over-after');
delete label.dataset.dropPos;
const from = draggingCol;
draggingCol = null;
if (!Number.isInteger(from) || from === c) return;
reorderCandidateCols(from, c, placeAfter);
renderColList();
doRefresh();
});
label.addEventListener('dragend', () => {
draggingCol = null;
clearDragVisuals();
});
const handle = document.createElement('span');
handle.className = 'drag-handle';
handle.textContent = '⋮⋮';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.draggable = false;
cb.checked = selectedCols.has(c);
cb.addEventListener('change', () => {
if (cb.checked) selectedCols.add(c);
else selectedCols.delete(c);
updateColSummary();
doRefresh();
});
const txt = document.createElement('span');
txt.className = 'txt';
const h = getHeaderText(sheet, c);
txt.textContent = `${colToLetter(c)}${h ? ` (${h})` : ''}`;
label.appendChild(handle);
label.appendChild(cb);
label.appendChild(txt);
colList.appendChild(label);
}
updateColSummary();
};
const clearColHeaderRetry = () => {
if (!colHeaderRetryTimer) return;
clearTimeout(colHeaderRetryTimer);
colHeaderRetryTimer = 0;
};
const getHeaderSignature = () => (
candidateCols.map((c) => `${c}:${getHeaderText(sheet, c)}`).join('|')
);
const hasMissingHeaders = () => candidateCols.some((c) => !getHeaderText(sheet, c));
const scheduleColHeaderRetry = () => {
clearColHeaderRetry();
if (!candidateCols.length) return;
if (!hasMissingHeaders()) return;
let attempts = 0;
let lastSig = getHeaderSignature();
const tick = () => {
colHeaderRetryTimer = 0;
attempts += 1;
const sig = getHeaderSignature();
if (sig !== lastSig) {
lastSig = sig;
renderColList();
}
if (!hasMissingHeaders()) return;
if (attempts >= COL_HEADER_RETRY_MAX_ATTEMPTS) return;
colHeaderRetryTimer = setTimeout(tick, COL_HEADER_RETRY_INTERVAL_MS);
};
colHeaderRetryTimer = setTimeout(tick, COL_HEADER_RETRY_INTERVAL_MS);
};
tryScheduleColHeaderRetry = scheduleColHeaderRetry;
const detectAndApplyCols = (mode = 'quick') => {
const runtimeDefaultCols = getTrackedColumns(sheet, getDefaultTargetColsByRuntime(app, sheet));
const isAliDocs = isAliDocsRuntime(app, sheet);
const useAliDocsManualDefault = isAliDocs && mode !== 'deep';
if (useAliDocsManualDefault) {
candidateCols = runtimeDefaultCols;
} else {
const hiddenCols = filterColumnsWithContent(sheet, detectHiddenColumns(sheet), {
maxScanRows: DEEP_SCAN_MAX_ROWS,
maxScanCells: DEEP_SCAN_MAX_CELLS
});
const appealCols = detectAppealBlockColumns(sheet);
if (hiddenCols.length) {
candidateCols = hiddenCols;
} else if (appealCols.length) {
candidateCols = appealCols;
} else {
if (mode === 'deep') {
const cols = detectMaskedColumns(sheet, {
maxScanRows: DEEP_SCAN_MAX_ROWS,
maxScanCells: DEEP_SCAN_MAX_CELLS,
includeFallbackSignals: true
});
candidateCols = cols.length ? cols : runtimeDefaultCols;
} else {
const quickCols = detectMaskedColumns(sheet, {
maxScanRows: QUICK_SCAN_MAX_ROWS,
maxScanCells: QUICK_SCAN_MAX_CELLS,
includeFallbackSignals: false
});
if (quickCols.length) {
candidateCols = quickCols;
} else {
const deepStarCols = detectMaskedColumns(sheet, {
maxScanRows: DEEP_SCAN_MAX_ROWS,
maxScanCells: DEEP_SCAN_MAX_CELLS,
includeFallbackSignals: false
});
candidateCols = deepStarCols.length ? deepStarCols : runtimeDefaultCols;
}
}
}
}
if (!useAliDocsManualDefault && mode === 'deep' && candidateCols.length && runtimeDefaultCols.length) {
const maxRow = sheet.getRowCount?.() || 0;
const activeRow0 =
tryExtractRowFromView(app, maxRow) ??
tryExtractRowFromDom(maxRow);
if (typeof activeRow0 === 'number') {
const hitDetected = hasAnyContentInRow(sheet, candidateCols, activeRow0);
if (!hitDetected) {
const hitDefault = hasAnyContentInRow(sheet, runtimeDefaultCols, activeRow0);
if (hitDefault) candidateCols = runtimeDefaultCols.slice();
}
}
}
candidateCols = useAliDocsManualDefault ? runtimeDefaultCols.slice() : applySavedOrder(candidateCols);
const candidateSet = new Set(candidateCols);
if (!selectedCols.size) selectedCols = new Set(candidateCols);
selectedCols = new Set([...selectedCols].filter((c) => candidateSet.has(c)));
if (!selectedCols.size && candidateCols.length) selectedCols = new Set(candidateCols);
persistCandidateOrder();
renderColList();
scheduleColHeaderRetry();
};
const renderCurrentRowOps = async (row0) => {
pendingOpsRow0 = Math.max(0, Number(row0) || 0);
const seq = ++opRenderSeq;
if (opNote) opNote.textContent = '正在读取当前行修订记录...';
if (opTbody) opTbody.innerHTML = '| 正在读取... |
';
const result = await runOpsFetchSerial(() => withTimeout(
() => fetchCurrentRowOperationRecords(app, sheet, row0),
30000,
{ ok: false, error: '修订读取超时' }
));
if (seq !== opRenderSeq) return;
if (!opTbody) return;
if (!result?.ok) {
const errMsg = String(result?.error || '未知错误');
if (isOpsUnavailableError(errMsg)) {
if (opNote) opNote.textContent = '当前行记录暂不可用,请稍后重试';
opTbody.innerHTML = '| 当前行暂无修订记录 |
';
return;
}
if (opNote) opNote.textContent = `读取失败: ${errMsg}`;
opTbody.innerHTML = '| 未获取到当前行修订记录 |
';
return;
}
const sortedRecords = (Array.isArray(result.records) ? result.records : [])
.slice()
.sort((a, b) => (Number(b?.modifiedDateTime) || 0) - (Number(a?.modifiedDateTime) || 0));
const dedupedRecords = dedupeOperationRecords(sortedRecords);
const dedupedCount = Math.max(0, sortedRecords.length - dedupedRecords.length);
const records = dedupedRecords.filter((item) => !shouldHideNoiseDetailRecord(item));
const hiddenNoiseCount = Math.max(0, dedupedRecords.length - records.length);
if (!records.length) {
if (opNote) {
const warning = String(result?.warning || '').trim();
const hiddenTag = hiddenNoiseCount > 0 ? `(已隐藏 ${hiddenNoiseCount} 条格式操作)` : '';
opNote.textContent = `${warning || '当前行暂无修订记录'}${hiddenTag}`;
}
opTbody.innerHTML = '| 当前行暂无修订记录 |
';
return;
}
if (opNote) {
const sourceTag =
result.source === 'changes' ? '(变更流)'
: result.source === 'localQueue' ? '(本地缓存)'
: '';
const dedupeTag = dedupedCount > 0 ? `(去重 ${dedupedCount} 条)` : '';
const hiddenTag = hiddenNoiseCount > 0 ? `(隐藏 ${hiddenNoiseCount} 条格式操作)` : '';
opNote.textContent = `当前行命中 ${records.length} 条操作记录${sourceTag}${dedupeTag}${hiddenTag}`;
}
const rowsHtml = records.map((item) => {
const hasCol = Number.isFinite(item.colIndex0) && item.colIndex0 >= 0;
const colLetter = hasCol ? colToLetter(item.colIndex0) : '-';
const colHeader = hasCol ? String(getHeaderText(sheet, item.colIndex0) || '').trim() : '';
const colText = colHeader || colLetter;
const colTitle = hasCol ? `${colLetter}${colHeader ? ` (${colHeader})` : ''}` : '-';
const detail = displayDetailText(item);
const detailText = detail || '(空)';
const authorText = String(item.authorName || item.authorId || '-').trim() || '-';
const authorClass = /^mi$/i.test(authorText) ? '' : 'tm-author-other';
return `
| ${escapeHtml(formatFullDateTime(item.modifiedDateTime) || '-')} |
${escapeHtml(colText)} |
${escapeHtml(actionTypeLabel(item.actionType))} |
${escapeHtml(authorText)} |
明细:${escapeHtml(detailText)} |
`;
}).join('');
opTbody.innerHTML = rowsHtml || '| 当前行暂无修订记录 |
';
};
refreshOpsForCurrentRow = () => renderCurrentRowOps(pendingOpsRow0);
const buildRowItemsForCols = (cols, row0) => {
return cols.map((c) => {
const colName = colToLetter(c);
const header = getHeaderText(sheet, c);
const colLabel = header || colName;
const cell = sheet.getCellDataAtPosition(row0, c, CELL_READ_OPTIONS);
const urls = extractCellUrls(cell);
const displayValue = normalizeCellText(displayTextOfCell(cell));
const value = normalizeCellText(textOfCell(cell));
return {
col0: c,
col: colName,
header,
colLabel,
displayValue,
value,
urls
};
});
};
const resolveRowItems = async (items) => {
return Promise.all((items || []).map(async (item) => {
const resolvedUrls = await resolveUrls(item.urls);
const value = resolvedUrls.length ? resolvedUrls.join(' | ') : (item.value || item.displayValue || '');
return { ...item, resolvedUrls, value };
}));
};
const filterVisibleRowItems = (items) => {
return (items || []).filter((item) => {
if (Array.isArray(item?.resolvedUrls) && item.resolvedUrls.length) return true;
if (normalizeCellText(item?.value || '') !== '') return true;
return normalizeCellText(item?.displayValue || '') !== '';
});
};
const buildAliDocsGroupRowCandidates = (baseRow0, maxRow) => {
const total = Math.max(0, Number(maxRow) || 0);
if (!total) return [];
const groupSize = Math.max(1, Number(DINGTALK_ROW_GROUP_SIZE) || 3);
const base = Math.max(0, Math.min(total - 1, Math.trunc(Number(baseRow0) || 0)));
const out = [];
const seen = new Set();
const push = (row) => {
const r = Math.trunc(Number(row));
if (!Number.isFinite(r) || r < 0 || r >= total || seen.has(r)) return;
seen.add(r);
out.push(r);
};
const groupStart = Math.floor(base / groupSize) * groupSize;
push(base);
for (let i = 0; i < groupSize; i += 1) push(groupStart + i);
for (let offset = 1; offset <= 2; offset += 1) {
const prevGroupStart = groupStart - offset * groupSize;
const nextGroupStart = groupStart + offset * groupSize;
for (let i = 0; i < groupSize; i += 1) push(prevGroupStart + i);
for (let i = 0; i < groupSize; i += 1) push(nextGroupStart + i);
}
return out;
};
const findRowFallbackItems = (row0, excludedCols, limit = 24) => {
const colCount = sheet.getColCount?.() || 0;
if (!colCount) return [];
const excluded = excludedCols instanceof Set ? excludedCols : new Set(excludedCols || []);
const candidates = [];
for (let c = 0; c < colCount; c += 1) {
if (excluded.has(c)) continue;
const cell = sheet.getCellDataAtPosition(row0, c, CELL_READ_OPTIONS);
const display = normalizeCellText(displayTextOfCell(cell));
const value = normalizeCellText(textOfCell(cell));
const urls = extractCellUrls(cell);
if (!urls.length && !value) continue;
const hidden = isColHidden(sheet, c);
const maskedReveal = looksMaskedDisplayText(display) && value && value !== display;
const priority = maskedReveal ? 0 : (hidden ? 1 : 2);
const colName = colToLetter(c);
const header = getHeaderText(sheet, c);
candidates.push({
col0: c,
col: colName,
header,
colLabel: header || colName,
displayValue: display,
value,
urls,
priority
});
}
candidates.sort((a, b) => (a.priority - b.priority) || (a.col0 - b.col0));
return candidates.slice(0, Math.max(1, Number(limit) || 24));
};
const isHiddenContentNoticeText = (text) => {
const s = normalizeCellText(text);
if (!s) return false;
if (/^此单元格已开启填写内容隐藏,你无法查看$/.test(s)) return true;
return /填写内容隐藏|你无法查看|无法查看|不可查看|不能查看/.test(s);
};
const readSelectionFormulaBarText = () => {
const comboboxes = Array.from(document.querySelectorAll('[role="combobox"]'));
for (const el of comboboxes) {
const text = normalizeCellText(
(typeof el?.value === 'string' ? el.value : '') ||
(typeof el?.getAttribute === 'function' ? (el.getAttribute('value') || '') : '') ||
(typeof el?.textContent === 'string' ? el.textContent : '')
);
if (!text) continue;
if (/^\d+%$/.test(text)) continue;
return text;
}
return '';
};
const isHiddenContentCellAt = (row0, col0) => {
const rowCount = sheet.getRowCount?.() || 0;
const colCount = sheet.getColCount?.() || 0;
if (!Number.isInteger(row0) || !Number.isInteger(col0)) return false;
if (row0 < 0 || col0 < 0 || row0 >= rowCount || col0 >= colCount) return false;
const cell = sheet.getCellDataAtPosition(row0, col0, CELL_READ_OPTIONS);
const display = normalizeCellText(displayTextOfCell(cell));
const reveal = normalizeCellText(textOfCell(cell));
if (isHiddenContentNoticeText(display) || isHiddenContentNoticeText(reveal)) return true;
if (display && looksMaskedDisplayText(display)) {
if (!reveal) return true;
return reveal !== display;
}
const formulaBarText = readSelectionFormulaBarText();
return isHiddenContentNoticeText(formulaBarText);
};
const render = async (row1, options = {}) => {
const force = options?.force === true;
const maxRow = sheet.getRowCount?.() || 0;
const row0 = Math.max(0, Math.min(maxRow - 1, row1 - 1));
const nextRow1 = row0 + 1;
if (!force && nextRow1 === currentRow1) {
scheduleReposition();
return;
}
currentRow1 = nextRow1;
pendingOpsRow0 = row0;
rowInput.value = String(currentRow1);
const bdEl = panel.querySelector('.bd');
if (bdEl) bdEl.scrollTop = 0;
if (activeTab === 'ops') renderCurrentRowOps(row0);
const renderSeq = ++valueRenderSeq;
const trackedCols = getOrderedSelectedCols();
if (!trackedCols.length) {
if (renderSeq !== valueRenderSeq) return;
tbody.innerHTML = '| 请先在“显示列”中选择列 |
';
const m = estimatePanelMetrics([]);
panel.style.setProperty('--tm-col-letter-width', `${m.colPx}px`);
panel.style.setProperty('--tm-col-value-width', `${m.valuePx}px`);
panel.style.setProperty('--tm-preview-width', `${m.valuePx}px`);
panel.style.width = `${m.panelPx}px`;
scheduleReposition();
return;
}
let dataRow0 = row0;
let rowData = await resolveRowItems(buildRowItemsForCols(trackedCols, dataRow0));
if (renderSeq !== valueRenderSeq) return;
let visibleData = filterVisibleRowItems(rowData);
let alignedByRowGroup = false;
if (!visibleData.length && isAliDocsRuntime(app, sheet)) {
const candidates = buildAliDocsGroupRowCandidates(row0, maxRow);
for (const candidateRow0 of candidates) {
if (candidateRow0 === dataRow0) continue;
const candidateRowData = await resolveRowItems(buildRowItemsForCols(trackedCols, candidateRow0));
if (renderSeq !== valueRenderSeq) return;
const candidateVisible = filterVisibleRowItems(candidateRowData);
if (!candidateVisible.length) continue;
dataRow0 = candidateRow0;
rowData = candidateRowData;
visibleData = candidateVisible;
alignedByRowGroup = true;
break;
}
}
let usedFallbackCols = false;
if (!visibleData.length && isAliDocsRuntime(app, sheet)) {
const fallbackItems = findRowFallbackItems(dataRow0, new Set(trackedCols), 24);
if (fallbackItems.length) {
const fallbackData = await resolveRowItems(fallbackItems);
if (renderSeq !== valueRenderSeq) return;
visibleData = filterVisibleRowItems(fallbackData);
usedFallbackCols = visibleData.length > 0;
}
}
tbody.innerHTML = '';
if (!visibleData.length) {
const tr = document.createElement('tr');
tr.innerHTML = `第 ${currentRow1} 行在当前显示列中无非空内容,请切换列或手动选列 | `;
tbody.appendChild(tr);
const m = estimatePanelMetrics([]);
panel.style.setProperty('--tm-col-letter-width', `${m.colPx}px`);
panel.style.setProperty('--tm-col-value-width', `${m.valuePx}px`);
panel.style.setProperty('--tm-preview-width', `${m.valuePx}px`);
panel.style.width = `${m.panelPx}px`;
scheduleReposition();
return;
}
if (alignedByRowGroup && dataRow0 !== row0) {
const tr = document.createElement('tr');
tr.innerHTML = `钉钉文档按每${DINGTALK_ROW_GROUP_SIZE}行取值,已对齐到第 ${dataRow0 + 1} 行 | `;
tbody.appendChild(tr);
}
if (usedFallbackCols) {
const tr = document.createElement('tr');
tr.innerHTML = '已自动补充当前行有值列(未命中已选列) | ';
tbody.appendChild(tr);
}
for (const item of visibleData) {
const tr = document.createElement('tr');
tr.innerHTML = `${escapeHtml(item.colLabel || item.col)} | ${valueHtml(item)} | `;
tbody.appendChild(tr);
}
const m = estimatePanelMetrics(visibleData);
panel.style.setProperty('--tm-col-letter-width', `${m.colPx}px`);
panel.style.setProperty('--tm-col-value-width', `${m.valuePx}px`);
panel.style.setProperty('--tm-preview-width', `${m.valuePx}px`);
panel.style.width = `${m.panelPx}px`;
scheduleReposition();
};
const getPreferredRefreshRow1 = () => {
const maxRow = sheet.getRowCount?.() || 0;
const manualRow = Number(rowInput.value);
const manualRowValid = Number.isFinite(manualRow) && manualRow > 0;
const activeEl = document.activeElement;
const isTypingRowInput = !!rowInput && (activeEl === rowInput || rowInput.contains?.(activeEl));
if (isTypingRowInput && manualRowValid) return Math.trunc(manualRow);
const activeRow0 =
tryExtractRowFromView(app, maxRow) ??
tryExtractRowFromDom(maxRow);
if (typeof activeRow0 === 'number') return activeRow0 + 1;
if (manualRowValid) return Math.trunc(manualRow);
return currentRow1;
};
doRefresh = () => {
const row = getPreferredRefreshRow1();
render(row, { force: true });
};
doDetectCols = () => {
if (btnDetect) btnDetect.disabled = true;
try {
detectAndApplyCols('deep');
doRefresh();
} finally {
if (btnDetect) btnDetect.disabled = false;
}
};
doRevealAll = async () => {
if (btnReveal) btnReveal.disabled = true;
stopRevealResize(false);
revealLastTSV = '';
revealRenderState = null;
setRevealDialogOpen(true);
if (revealTable) revealTable.innerHTML = '';
if (revealMeta) revealMeta.textContent = '正在准备全表显示...';
try {
const result = await buildRevealTable();
if (!result?.ok) {
if (revealMeta) revealMeta.textContent = String(result?.message || '生成失败');
return;
}
if (revealMeta) revealMeta.textContent = result.meta || `已显示 ${result.rows || 0} 行`;
} catch (err) {
if (revealMeta) revealMeta.textContent = `生成失败: ${String(err?.message || err)}`;
} finally {
if (btnReveal) btnReveal.disabled = false;
}
};
const applyManualSelection = () => {
const colCount = sheet.getColCount?.() || 0;
if (!colCount) return;
const raw = String(colManualInput?.value || '').trim();
const cols = parseColumnSelection(raw, colCount);
if (!cols.length) {
if (colManualError) colManualError.textContent = '未识别到有效列,请输入如 A,C,F 或 A:F';
if (colManualInput) {
colManualInput.focus();
colManualInput.select();
}
return;
}
ensureCandidateCols(cols);
selectedCols = new Set(cols);
persistCandidateOrder();
renderColList();
setManualDialogOpen(false);
doRefresh();
};
doManualSelectCols = () => {
const preset = getOrderedSelectedCols().map(colToLetter).join(',');
setColPickerOpen(false);
setManualDialogOpen(true, preset);
};
doSelectAllCols = () => {
selectedCols = new Set(candidateCols);
renderColList();
doRefresh();
};
doClearCols = () => {
selectedCols = new Set();
renderColList();
doRefresh();
};
colManualCancel?.addEventListener('click', () => setManualDialogOpen(false));
colManualOk?.addEventListener('click', () => applyManualSelection());
colManualInput?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
applyManualSelection();
return;
}
if (e.key === 'Escape') {
e.preventDefault();
setManualDialogOpen(false);
}
});
colManualModal?.addEventListener('mousedown', (e) => {
const target = e.target;
if (!(target instanceof Element)) return;
if (target.closest('[data-role=\"close\"]')) setManualDialogOpen(false);
});
revealClose?.addEventListener('click', () => setRevealDialogOpen(false));
revealCopy?.addEventListener('click', () => {
if (!revealLastTSV) return;
try {
GM_setClipboard(revealLastTSV);
if (revealMeta) revealMeta.textContent = `${revealMeta.textContent || ''}(已复制)`;
} catch (_) { }
});
revealModal?.addEventListener('mousedown', (e) => {
const target = e.target;
if (!(target instanceof Element)) return;
if (target.closest('[data-role=\"close-reveal\"]')) setRevealDialogOpen(false);
});
revealTable?.addEventListener('click', (e) => {
const target = e.target;
if (!(target instanceof Element)) return;
const sortBtn = target.closest('[data-role=\"reveal-sort-row\"]');
if (!sortBtn) return;
revealSortOrder = revealSortOrder === 'desc' ? 'asc' : 'desc';
persistRevealView();
renderRevealTableFromState({ keepScroll: true });
});
revealTable?.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
const target = e.target;
if (!(target instanceof Element)) return;
const handle = target.closest('[data-role=\"reveal-col-resize\"]');
if (!handle) return;
const key = String(handle.getAttribute('data-key') || '').trim();
if (!key) return;
const th = handle.closest('th');
const baseWidth = normalizeRevealColWidth(th?.getBoundingClientRect?.().width || 0) || getRevealColWidth(key);
revealResizeSession = {
key,
startX: Number(e.clientX || 0),
startWidth: baseWidth,
prevUserSelect: document.documentElement.style.userSelect || ''
};
document.documentElement.style.userSelect = 'none';
panel.classList.add('reveal-resizing');
e.preventDefault();
e.stopPropagation();
});
doToggle = () => {
clearColHeaderRetry();
setColPickerOpen(false);
setManualDialogOpen(false);
setRevealDialogOpen(false);
panel.classList.add('hidden');
};
doReposition = () => {
placePanelNearSelection(panel);
if (colPicker?.classList.contains('open')) placeColPickerPop();
};
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
if (isRevealDialogOpen()) {
setRevealDialogOpen(false);
return;
}
if (isManualDialogOpen()) {
setManualDialogOpen(false);
return;
}
setColPickerOpen(false);
return;
}
// Alt + H 切换面板
if (e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey && (e.key === 'h' || e.key === 'H')) {
panel.classList.toggle('hidden');
if (panel.classList.contains('hidden')) {
clearColHeaderRetry();
setColPickerOpen(false);
setManualDialogOpen(false);
setRevealDialogOpen(false);
}
if (!panel.classList.contains('hidden')) scheduleReposition();
}
// Alt + R 刷新当前行
if (e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey && (e.key === 'r' || e.key === 'R')) {
doRefresh();
}
});
// 保留“点击表格行 => 面板自动显示对应行”的联动能力
try {
if (typeof app.view?.onSelectionChange === 'function') {
app.view.onSelectionChange((payload) => {
const maxRow = sheet.getRowCount?.() || 1;
const maxCol = sheet.getColCount?.() || 0;
const row0 =
tryExtractRowFromSelectionPayload(payload, maxRow) ??
tryExtractRowFromView(app, maxRow) ??
tryExtractRowFromDom(maxRow);
const col0 = maxCol > 0
? (
tryExtractColFromSelectionPayload(payload, maxCol) ??
tryExtractColFromView(app, maxCol)
)
: null;
if (typeof row0 === 'number') {
pendingSelectionRow1 = row0 + 1;
pendingSelectionCol0 = typeof col0 === 'number' ? col0 : null;
if (selectionRenderTimer) return;
selectionRenderTimer = setTimeout(() => {
selectionRenderTimer = 0;
const row1 = pendingSelectionRow1;
const col = pendingSelectionCol0;
const force = typeof row1 === 'number' && typeof col === 'number'
? isHiddenContentCellAt(row1 - 1, col)
: false;
pendingSelectionRow1 = null;
pendingSelectionCol0 = null;
if (force) {
setRevealDialogOpen(false);
if (panel.classList.contains('hidden')) panel.classList.remove('hidden');
}
const shouldAutoHideOnPlainSelection = !isAliDocsRuntime(app, sheet);
if (!force && shouldAutoHideOnPlainSelection && !panel.classList.contains('hidden')) {
clearColHeaderRetry();
setColPickerOpen(false);
setManualDialogOpen(false);
setRevealDialogOpen(false);
panel.classList.add('hidden');
return;
}
const shouldRender = force || !panel.classList.contains('hidden');
if (typeof row1 === 'number' && shouldRender) render(row1, { force });
}, SELECTION_RENDER_DEBOUNCE_MS);
}
else {
pendingSelectionCol0 = null;
doReposition();
}
});
}
} catch (_) { }
detectAndApplyCols('quick');
resolveInitialRow1().then((row1) => {
currentRow1 = row1;
render(currentRow1, { force: true });
});
};
let initRetryCount = 0;
const tryInit = () => {
waitForSpreadsheetApp(5000).then((ctx) => {
initRetryCount = 0;
initDataLayer(ctx);
}).catch((err) => {
if (initialized) return;
initRetryCount += 1;
if (initRetryCount >= INIT_RETRY_MAX_ATTEMPTS) {
console.warn('[hidden-col-viewer] init aborted after max retries:', err?.message || err);
return;
}
console.warn(
`[hidden-col-viewer] retry init (${initRetryCount}/${INIT_RETRY_MAX_ATTEMPTS}):`,
err?.message || err
);
setTimeout(tryInit, 2000);
});
};
tryInit();
}
const isAliDocsPortalTopPage = (
location.hostname === 'alidocs.dingtalk.com' &&
ALIDOCS_PORTAL_PATH_RE.test(location.pathname) &&
PAGE_WIN.top === PAGE_WIN
);
if (isAliDocsPortalTopPage) {
console.info('[hidden-col-viewer] skip portal shell page, runtime will init inside spreadsheet iframe.');
return;
}
if (document.readyState === 'complete') install();
else window.addEventListener('load', install, { once: true });
})();