@flrande/bak-extension 0.6.11 → 0.6.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.bak-e2e-build-stamp +1 -1
- package/dist/background.global.js +588 -90
- package/dist/content.global.js +530 -12
- package/dist/manifest.json +1 -1
- package/dist/popup.global.js +233 -101
- package/dist/popup.html +260 -37
- package/package.json +2 -2
- package/public/popup.html +260 -37
- package/src/background.ts +344 -296
- package/src/content.ts +390 -22
- package/src/dynamic-data-tools.ts +790 -0
- package/src/popup.ts +291 -108
package/src/content.ts
CHANGED
|
@@ -11,11 +11,22 @@ import type {
|
|
|
11
11
|
PageMetrics,
|
|
12
12
|
PageTextChunk,
|
|
13
13
|
TableColumn,
|
|
14
|
-
|
|
14
|
+
TableExtractionMetadata,
|
|
15
|
+
TableHandle,
|
|
16
|
+
TableIntelligence
|
|
15
17
|
} from '@flrande/bak-protocol';
|
|
16
|
-
import { documentMetadata, isDocumentNode, isShadowRootNode } from './context-metadata.js';
|
|
18
|
+
import { documentMetadata, isDocumentNode, isShadowRootNode } from './context-metadata.js';
|
|
19
|
+
import {
|
|
20
|
+
buildExtractionMetadata,
|
|
21
|
+
buildTableIntelligence,
|
|
22
|
+
collectTimestampProbes,
|
|
23
|
+
estimateSampleSize,
|
|
24
|
+
inferSchemaHint,
|
|
25
|
+
sampleValue,
|
|
26
|
+
type InlineJsonInspectionSource
|
|
27
|
+
} from './dynamic-data-tools.js';
|
|
17
28
|
import { inferSafeName, redactElementText, redactHtmlSnapshot, type RedactTextOptions } from './privacy.js';
|
|
18
|
-
import { unsupportedLocatorHint } from './limitations.js';
|
|
29
|
+
import { unsupportedLocatorHint } from './limitations.js';
|
|
19
30
|
|
|
20
31
|
type ActionName =
|
|
21
32
|
| 'click'
|
|
@@ -1785,6 +1796,135 @@ function buildTableId(kind: TableHandle['kind'], index: number): string {
|
|
|
1785
1796
|
return `${kind}:${index + 1}`;
|
|
1786
1797
|
}
|
|
1787
1798
|
|
|
1799
|
+
function gridRowNodes(grid: Element): HTMLElement[] {
|
|
1800
|
+
return Array.from(grid.querySelectorAll<HTMLElement>('[role="row"]')).filter(
|
|
1801
|
+
(row) => row.querySelector('[role="gridcell"], [role="cell"]') !== null
|
|
1802
|
+
);
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
function tableSchemaFromElement(element: Element): TableColumn[] {
|
|
1806
|
+
return element instanceof HTMLTableElement ? htmlTableSchema(element) : gridSchema(element);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
function tableRowsFromElement(element: Element, limit: number): Array<Record<string, unknown>> {
|
|
1810
|
+
return element instanceof HTMLTableElement ? htmlTableRows(element, limit) : gridRows(element, limit);
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function findScrollableTableContainer(element: Element): HTMLElement | null {
|
|
1814
|
+
const explicitCandidate =
|
|
1815
|
+
(isHtmlElement(element) && (element.matches('[data-bak-scroll-root], [data-virtual-scroll]') ? element : null)) ||
|
|
1816
|
+
('querySelector' in element
|
|
1817
|
+
? (element.querySelector<HTMLElement>('[data-bak-scroll-root], [data-virtual-scroll]') ?? null)
|
|
1818
|
+
: null);
|
|
1819
|
+
if (explicitCandidate) {
|
|
1820
|
+
return explicitCandidate;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
const candidates: HTMLElement[] = [];
|
|
1824
|
+
if (isHtmlElement(element)) {
|
|
1825
|
+
candidates.push(element);
|
|
1826
|
+
}
|
|
1827
|
+
if ('querySelectorAll' in element) {
|
|
1828
|
+
candidates.push(...Array.from(element.querySelectorAll<HTMLElement>('[role="rowgroup"], *')));
|
|
1829
|
+
}
|
|
1830
|
+
let current: HTMLElement | null = element.parentElement;
|
|
1831
|
+
while (current && candidates.length < 80) {
|
|
1832
|
+
candidates.push(current);
|
|
1833
|
+
current = current.parentElement;
|
|
1834
|
+
}
|
|
1835
|
+
const uniqueCandidates = [...new Set(candidates)];
|
|
1836
|
+
return (
|
|
1837
|
+
uniqueCandidates.find((candidate) => {
|
|
1838
|
+
const style = getComputedStyle(candidate);
|
|
1839
|
+
return (
|
|
1840
|
+
(style.overflowY === 'auto' || style.overflowY === 'scroll') &&
|
|
1841
|
+
candidate.scrollHeight > candidate.clientHeight + 4 &&
|
|
1842
|
+
candidate.clientHeight > 0
|
|
1843
|
+
);
|
|
1844
|
+
}) ?? null
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
function observedRowIndexes(rows: HTMLElement[]): number[] {
|
|
1849
|
+
return rows
|
|
1850
|
+
.map((row) => {
|
|
1851
|
+
const raw = row.getAttribute('aria-rowindex') || row.getAttribute('data-row-index') || row.dataset.rowIndex;
|
|
1852
|
+
const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
|
|
1853
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
1854
|
+
})
|
|
1855
|
+
.filter((value): value is number => value !== null);
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
function inferEstimatedTotalRows(element: Element, rows: HTMLElement[], scrollContainer: HTMLElement | null): number | undefined {
|
|
1859
|
+
const attrCandidates = [
|
|
1860
|
+
element.getAttribute('aria-rowcount'),
|
|
1861
|
+
(scrollContainer ?? undefined)?.getAttribute('aria-rowcount'),
|
|
1862
|
+
element.getAttribute('data-total-rows'),
|
|
1863
|
+
(scrollContainer ?? undefined)?.getAttribute('data-total-rows')
|
|
1864
|
+
]
|
|
1865
|
+
.filter((candidate): candidate is string => typeof candidate === 'string' && candidate.trim().length > 0)
|
|
1866
|
+
.map((candidate) => Number.parseInt(candidate, 10))
|
|
1867
|
+
.filter((candidate) => Number.isFinite(candidate) && candidate > 0);
|
|
1868
|
+
if (attrCandidates.length > 0) {
|
|
1869
|
+
return Math.max(...attrCandidates);
|
|
1870
|
+
}
|
|
1871
|
+
const indexes = observedRowIndexes(rows);
|
|
1872
|
+
if (indexes.length > 0) {
|
|
1873
|
+
return Math.max(...indexes);
|
|
1874
|
+
}
|
|
1875
|
+
const rowHeight = rows[0]?.getBoundingClientRect().height ?? 0;
|
|
1876
|
+
if (scrollContainer && rowHeight > 0) {
|
|
1877
|
+
const estimate = Math.round(scrollContainer.scrollHeight / rowHeight);
|
|
1878
|
+
if (estimate > rows.length) {
|
|
1879
|
+
return estimate;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
return undefined;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
function tableIntelligenceForElement(table: TableHandle, element: Element): TableIntelligence {
|
|
1886
|
+
if (element instanceof HTMLTableElement) {
|
|
1887
|
+
return buildTableIntelligence({
|
|
1888
|
+
kind: table.kind,
|
|
1889
|
+
visibleRowCount: htmlTableRows(element, Number.MAX_SAFE_INTEGER).length,
|
|
1890
|
+
rowCount: table.rowCount,
|
|
1891
|
+
estimatedTotalRows: table.rowCount,
|
|
1892
|
+
hasScrollContainer: false,
|
|
1893
|
+
hasTranslatedRows: false,
|
|
1894
|
+
knownGridKind: false
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
const rows = gridRowNodes(element);
|
|
1898
|
+
const scrollContainer = findScrollableTableContainer(element);
|
|
1899
|
+
const rowIndexes = observedRowIndexes(rows);
|
|
1900
|
+
const estimatedTotalRows = inferEstimatedTotalRows(element, rows, scrollContainer);
|
|
1901
|
+
const hasTranslatedRows = rows.some((row) => {
|
|
1902
|
+
const style = row.style.transform || getComputedStyle(row).transform;
|
|
1903
|
+
return typeof style === 'string' && style !== '' && style !== 'none';
|
|
1904
|
+
});
|
|
1905
|
+
return buildTableIntelligence({
|
|
1906
|
+
kind: table.kind,
|
|
1907
|
+
visibleRowCount: rows.length,
|
|
1908
|
+
rowCount: table.rowCount,
|
|
1909
|
+
estimatedTotalRows,
|
|
1910
|
+
hasScrollContainer: scrollContainer !== null,
|
|
1911
|
+
hasTranslatedRows,
|
|
1912
|
+
maxObservedRowIndex: rowIndexes.length > 0 ? Math.max(...rowIndexes) : undefined,
|
|
1913
|
+
minObservedRowIndex: rowIndexes.length > 0 ? Math.min(...rowIndexes) : undefined,
|
|
1914
|
+
knownGridKind: table.kind !== 'aria-grid'
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
function withTableIntelligence(table: TableHandle, element: Element | null): TableHandle {
|
|
1919
|
+
if (!(element instanceof Element)) {
|
|
1920
|
+
return table;
|
|
1921
|
+
}
|
|
1922
|
+
return {
|
|
1923
|
+
...table,
|
|
1924
|
+
intelligence: tableIntelligenceForElement(table, element)
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1788
1928
|
function describeTables(): TableHandle[] {
|
|
1789
1929
|
const rootResult = resolveRootForLocator();
|
|
1790
1930
|
if (!rootResult.ok) {
|
|
@@ -1794,26 +1934,28 @@ function describeTables(): TableHandle[] {
|
|
|
1794
1934
|
const tables: TableHandle[] = [];
|
|
1795
1935
|
const htmlTables = Array.from(root.querySelectorAll('table'));
|
|
1796
1936
|
for (const [index, table] of htmlTables.entries()) {
|
|
1797
|
-
|
|
1937
|
+
const handle: TableHandle = {
|
|
1798
1938
|
id: buildTableId(table.closest('.dataTables_wrapper') ? 'dataTables' : 'html', index),
|
|
1799
1939
|
name: (table.getAttribute('aria-label') || table.getAttribute('data-testid') || table.id || `table-${index + 1}`).trim(),
|
|
1800
1940
|
kind: table.closest('.dataTables_wrapper') ? 'dataTables' : 'html',
|
|
1801
1941
|
selector: table.id ? `#${table.id}` : undefined,
|
|
1802
1942
|
rowCount: table.querySelectorAll('tbody tr').length || table.querySelectorAll('tr').length,
|
|
1803
1943
|
columnCount: table.querySelectorAll('thead th').length || table.querySelectorAll('tr:first-child th, tr:first-child td').length
|
|
1804
|
-
}
|
|
1944
|
+
};
|
|
1945
|
+
tables.push(withTableIntelligence(handle, table));
|
|
1805
1946
|
}
|
|
1806
1947
|
const gridRoots = Array.from(root.querySelectorAll<HTMLElement>('[role="grid"], [role="table"], .ag-root, .ag-root-wrapper'));
|
|
1807
1948
|
for (const [index, grid] of gridRoots.entries()) {
|
|
1808
1949
|
const kind: TableHandle['kind'] = grid.className.includes('ag-') ? 'ag-grid' : 'aria-grid';
|
|
1809
|
-
|
|
1950
|
+
const handle: TableHandle = {
|
|
1810
1951
|
id: buildTableId(kind, index),
|
|
1811
1952
|
name: (grid.getAttribute('aria-label') || grid.getAttribute('data-testid') || grid.id || `grid-${index + 1}`).trim(),
|
|
1812
1953
|
kind,
|
|
1813
1954
|
selector: grid.id ? `#${grid.id}` : undefined,
|
|
1814
|
-
rowCount: grid
|
|
1955
|
+
rowCount: gridRowNodes(grid).length,
|
|
1815
1956
|
columnCount: grid.querySelectorAll('[role="columnheader"]').length
|
|
1816
|
-
}
|
|
1957
|
+
};
|
|
1958
|
+
tables.push(withTableIntelligence(handle, grid));
|
|
1817
1959
|
}
|
|
1818
1960
|
return tables;
|
|
1819
1961
|
}
|
|
@@ -1878,11 +2020,11 @@ function gridSchema(grid: Element): TableColumn[] {
|
|
|
1878
2020
|
|
|
1879
2021
|
function gridRows(grid: Element, limit: number): Array<Record<string, unknown>> {
|
|
1880
2022
|
const columns = gridSchema(grid);
|
|
1881
|
-
return
|
|
2023
|
+
return gridRowNodes(grid)
|
|
1882
2024
|
.slice(0, limit)
|
|
1883
2025
|
.map((row) => {
|
|
1884
2026
|
const record: Record<string, unknown> = {};
|
|
1885
|
-
Array.from(row.querySelectorAll('[role="gridcell"], [role="cell"]
|
|
2027
|
+
Array.from(row.querySelectorAll('[role="gridcell"], [role="cell"]')).forEach((cell, index) => {
|
|
1886
2028
|
const key = columns[index]?.label ?? `Column ${index + 1}`;
|
|
1887
2029
|
record[key] = (cell.textContent ?? '').trim();
|
|
1888
2030
|
});
|
|
@@ -1891,6 +2033,187 @@ function gridRows(grid: Element, limit: number): Array<Record<string, unknown>>
|
|
|
1891
2033
|
.filter((row) => Object.values(row).some((value) => String(value).trim().length > 0));
|
|
1892
2034
|
}
|
|
1893
2035
|
|
|
2036
|
+
function rowSignature(row: Record<string, unknown>): string {
|
|
2037
|
+
return JSON.stringify(
|
|
2038
|
+
Object.entries(row)
|
|
2039
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
2040
|
+
.map(([key, value]) => [key, value === undefined ? null : value])
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
function mergeUniqueRows(target: Map<string, Record<string, unknown>>, rows: Array<Record<string, unknown>>, limit: number): number {
|
|
2045
|
+
let added = 0;
|
|
2046
|
+
for (const row of rows) {
|
|
2047
|
+
const signature = rowSignature(row);
|
|
2048
|
+
if (target.has(signature)) {
|
|
2049
|
+
continue;
|
|
2050
|
+
}
|
|
2051
|
+
target.set(signature, row);
|
|
2052
|
+
added += 1;
|
|
2053
|
+
if (target.size >= limit) {
|
|
2054
|
+
break;
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
return added;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function waitForPaint(frames = 2): Promise<void> {
|
|
2061
|
+
return new Promise((resolve) => {
|
|
2062
|
+
const schedule = (remaining: number): void => {
|
|
2063
|
+
if (remaining <= 0) {
|
|
2064
|
+
window.setTimeout(() => resolve(), 24);
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
requestAnimationFrame(() => schedule(remaining - 1));
|
|
2068
|
+
};
|
|
2069
|
+
schedule(frames);
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
function currentTableWindowSignature(element: Element): string {
|
|
2074
|
+
const rowIndexes = observedRowIndexes(gridRowNodes(element));
|
|
2075
|
+
if (rowIndexes.length > 0) {
|
|
2076
|
+
return `indexes:${rowIndexes.join(',')}`;
|
|
2077
|
+
}
|
|
2078
|
+
const visibleRows = tableRowsFromElement(element, 24);
|
|
2079
|
+
return `rows:${visibleRows.map((row) => rowSignature(row)).join('|')}`;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
async function waitForTableWindowChange(
|
|
2083
|
+
element: Element,
|
|
2084
|
+
previousSignature: string,
|
|
2085
|
+
timeoutMs = 200
|
|
2086
|
+
): Promise<string> {
|
|
2087
|
+
const start = Date.now();
|
|
2088
|
+
let currentSignature = previousSignature;
|
|
2089
|
+
while (Date.now() - start < timeoutMs) {
|
|
2090
|
+
await waitForPaint(1);
|
|
2091
|
+
currentSignature = currentTableWindowSignature(element);
|
|
2092
|
+
if (currentSignature !== previousSignature) {
|
|
2093
|
+
return currentSignature;
|
|
2094
|
+
}
|
|
2095
|
+
await new Promise((resolve) => window.setTimeout(resolve, 16));
|
|
2096
|
+
}
|
|
2097
|
+
return currentSignature;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
async function scrollExtractRows(
|
|
2101
|
+
element: Element,
|
|
2102
|
+
limit: number,
|
|
2103
|
+
intelligence?: TableIntelligence
|
|
2104
|
+
): Promise<{ rows: Array<Record<string, unknown>>; extraction: TableExtractionMetadata; extractionMode: TableExtractionMetadata['mode'] }> {
|
|
2105
|
+
const scrollContainer = findScrollableTableContainer(element);
|
|
2106
|
+
if (!scrollContainer) {
|
|
2107
|
+
const rows = tableRowsFromElement(element, limit);
|
|
2108
|
+
return {
|
|
2109
|
+
rows,
|
|
2110
|
+
extractionMode: 'visibleOnly',
|
|
2111
|
+
extraction: buildExtractionMetadata('visibleOnly', rows, intelligence, [
|
|
2112
|
+
'Fell back to visible rows because no scroll container was detected.'
|
|
2113
|
+
])
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
const originalScrollTop = scrollContainer.scrollTop;
|
|
2118
|
+
const collected = new Map<string, Record<string, unknown>>();
|
|
2119
|
+
const seenRowIndexes = new Set<number>();
|
|
2120
|
+
let stagnantPasses = 0;
|
|
2121
|
+
let reachedEnd = false;
|
|
2122
|
+
let limitApplied = false;
|
|
2123
|
+
const rowHeight = gridRowNodes(element)[0]?.getBoundingClientRect().height ?? 0;
|
|
2124
|
+
const step = Math.max(80, Math.round(scrollContainer.clientHeight * 0.8), rowHeight > 0 ? Math.round(rowHeight * 4) : 0);
|
|
2125
|
+
const initialMaxScrollTop = Math.max(0, scrollContainer.scrollHeight - scrollContainer.clientHeight);
|
|
2126
|
+
const maxIterations = Math.max(8, Math.min(80, Math.ceil(initialMaxScrollTop / Math.max(step, 1)) + 6, limit * 2));
|
|
2127
|
+
|
|
2128
|
+
try {
|
|
2129
|
+
scrollContainer.scrollTop = 0;
|
|
2130
|
+
scrollContainer.dispatchEvent(new Event('scroll'));
|
|
2131
|
+
await waitForPaint();
|
|
2132
|
+
let currentWindowSignature = currentTableWindowSignature(element);
|
|
2133
|
+
for (let iteration = 0; iteration < maxIterations; iteration += 1) {
|
|
2134
|
+
const currentRows = tableRowsFromElement(element, limit);
|
|
2135
|
+
const added = mergeUniqueRows(collected, currentRows, limit);
|
|
2136
|
+
observedRowIndexes(gridRowNodes(element)).forEach((rowIndex) => {
|
|
2137
|
+
seenRowIndexes.add(rowIndex);
|
|
2138
|
+
});
|
|
2139
|
+
const estimatedTotalRows = intelligence?.estimatedTotalRows;
|
|
2140
|
+
if (
|
|
2141
|
+
typeof estimatedTotalRows === 'number' &&
|
|
2142
|
+
(collected.size >= estimatedTotalRows || seenRowIndexes.size >= estimatedTotalRows)
|
|
2143
|
+
) {
|
|
2144
|
+
reachedEnd = true;
|
|
2145
|
+
break;
|
|
2146
|
+
}
|
|
2147
|
+
if (collected.size >= limit) {
|
|
2148
|
+
limitApplied = true;
|
|
2149
|
+
break;
|
|
2150
|
+
}
|
|
2151
|
+
if (added === 0) {
|
|
2152
|
+
stagnantPasses += 1;
|
|
2153
|
+
} else {
|
|
2154
|
+
stagnantPasses = 0;
|
|
2155
|
+
}
|
|
2156
|
+
const maxScrollTop = Math.max(0, scrollContainer.scrollHeight - scrollContainer.clientHeight);
|
|
2157
|
+
if (scrollContainer.scrollTop >= maxScrollTop - 2) {
|
|
2158
|
+
reachedEnd = true;
|
|
2159
|
+
if (stagnantPasses >= 1) {
|
|
2160
|
+
break;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
if (stagnantPasses >= 2) {
|
|
2164
|
+
break;
|
|
2165
|
+
}
|
|
2166
|
+
const nextScrollTop = Math.min(maxScrollTop, scrollContainer.scrollTop + step);
|
|
2167
|
+
if (nextScrollTop === scrollContainer.scrollTop) {
|
|
2168
|
+
reachedEnd = true;
|
|
2169
|
+
break;
|
|
2170
|
+
}
|
|
2171
|
+
scrollContainer.scrollTop = nextScrollTop;
|
|
2172
|
+
scrollContainer.dispatchEvent(new Event('scroll'));
|
|
2173
|
+
currentWindowSignature = await waitForTableWindowChange(element, currentWindowSignature);
|
|
2174
|
+
}
|
|
2175
|
+
if (!reachedEnd) {
|
|
2176
|
+
const maxScrollTop = Math.max(0, scrollContainer.scrollHeight - scrollContainer.clientHeight);
|
|
2177
|
+
if (scrollContainer.scrollTop < maxScrollTop) {
|
|
2178
|
+
const previousWindowSignature = currentTableWindowSignature(element);
|
|
2179
|
+
scrollContainer.scrollTop = maxScrollTop;
|
|
2180
|
+
scrollContainer.dispatchEvent(new Event('scroll'));
|
|
2181
|
+
await waitForTableWindowChange(element, previousWindowSignature);
|
|
2182
|
+
mergeUniqueRows(collected, tableRowsFromElement(element, limit), limit);
|
|
2183
|
+
observedRowIndexes(gridRowNodes(element)).forEach((rowIndex) => {
|
|
2184
|
+
seenRowIndexes.add(rowIndex);
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
} finally {
|
|
2189
|
+
scrollContainer.scrollTop = originalScrollTop;
|
|
2190
|
+
await waitForPaint();
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
const rows = [...collected.values()].slice(0, limit);
|
|
2194
|
+
if (
|
|
2195
|
+
typeof intelligence?.estimatedTotalRows === 'number' &&
|
|
2196
|
+
(rows.length >= intelligence.estimatedTotalRows || seenRowIndexes.size >= intelligence.estimatedTotalRows)
|
|
2197
|
+
) {
|
|
2198
|
+
reachedEnd = true;
|
|
2199
|
+
}
|
|
2200
|
+
const warnings: string[] = [];
|
|
2201
|
+
if (limitApplied) {
|
|
2202
|
+
warnings.push('Stopped after reaching maxRows before confirming the end of the table.');
|
|
2203
|
+
}
|
|
2204
|
+
if (!reachedEnd && (intelligence?.estimatedTotalRows ?? rows.length) > rows.length) {
|
|
2205
|
+
warnings.push('Could not confirm the end of scrollable content; result may be partial.');
|
|
2206
|
+
}
|
|
2207
|
+
return {
|
|
2208
|
+
rows,
|
|
2209
|
+
extractionMode: 'scroll',
|
|
2210
|
+
extraction: buildExtractionMetadata('scroll', rows, intelligence, warnings, {
|
|
2211
|
+
reachedEnd,
|
|
2212
|
+
limitApplied
|
|
2213
|
+
})
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
|
|
1894
2217
|
function runPageWorldRequest<T>(action: string, payload: Record<string, unknown>): Promise<T> {
|
|
1895
2218
|
const requestId = `bak_page_world_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
1896
2219
|
return new Promise<T>((resolve, reject) => {
|
|
@@ -2284,6 +2607,34 @@ async function globalsPreview(): Promise<string[]> {
|
|
|
2284
2607
|
.slice(0, 50);
|
|
2285
2608
|
}
|
|
2286
2609
|
|
|
2610
|
+
function collectInlineJsonSources(root: ParentNode): InlineJsonInspectionSource[] {
|
|
2611
|
+
const scripts = Array.from(root.querySelectorAll<HTMLScriptElement>('script[type="application/json"], script[type="application/ld+json"]'));
|
|
2612
|
+
return scripts
|
|
2613
|
+
.map((script, index) => {
|
|
2614
|
+
const raw = script.textContent?.trim() ?? '';
|
|
2615
|
+
if (!raw) {
|
|
2616
|
+
return null;
|
|
2617
|
+
}
|
|
2618
|
+
try {
|
|
2619
|
+
const parsed = JSON.parse(raw);
|
|
2620
|
+
const path = script.id ? `#${script.id}` : `inline-json:${index + 1}`;
|
|
2621
|
+
const timestamps = collectTimestampProbes(parsed, path);
|
|
2622
|
+
return {
|
|
2623
|
+
label: script.id || script.getAttribute('data-testid') || script.type || `inline-json-${index + 1}`,
|
|
2624
|
+
path,
|
|
2625
|
+
sample: sampleValue(parsed),
|
|
2626
|
+
sampleSize: estimateSampleSize(parsed),
|
|
2627
|
+
schemaHint: inferSchemaHint(parsed),
|
|
2628
|
+
lastObservedAt: timestamps.length > 0 ? Math.max(...timestamps.map((item) => Date.parse(item.value)).filter(Number.isFinite)) : null,
|
|
2629
|
+
timestamps
|
|
2630
|
+
} satisfies InlineJsonInspectionSource;
|
|
2631
|
+
} catch {
|
|
2632
|
+
return null;
|
|
2633
|
+
}
|
|
2634
|
+
})
|
|
2635
|
+
.filter((item): item is InlineJsonInspectionSource => item !== null);
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2287
2638
|
async function collectInspectionState(params: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
|
|
2288
2639
|
const rootResult = resolveRootForLocator();
|
|
2289
2640
|
if (!rootResult.ok) {
|
|
@@ -2304,6 +2655,7 @@ async function collectInspectionState(params: Record<string, unknown> = {}): Pro
|
|
|
2304
2655
|
);
|
|
2305
2656
|
const previewGlobals = await globalsPreview();
|
|
2306
2657
|
const suspiciousGlobals = [...new Set(scripts.flatMap((script) => script.suspectedVars))].slice(0, 50);
|
|
2658
|
+
const inlineJsonSources = collectInlineJsonSources(root);
|
|
2307
2659
|
return {
|
|
2308
2660
|
url: metadata.url,
|
|
2309
2661
|
title: metadata.title,
|
|
@@ -2322,6 +2674,7 @@ async function collectInspectionState(params: Record<string, unknown> = {}): Pro
|
|
|
2322
2674
|
storage: storageMetadata(),
|
|
2323
2675
|
cookies: cookieMetadata(),
|
|
2324
2676
|
frames: collectFrames(),
|
|
2677
|
+
inlineJsonSources,
|
|
2325
2678
|
tables: describeTables(),
|
|
2326
2679
|
timers: {
|
|
2327
2680
|
timeouts: 0,
|
|
@@ -3076,12 +3429,10 @@ async function dispatchRpc(method: string, params: Record<string, unknown> = {})
|
|
|
3076
3429
|
if (!resolved || !(resolved.element instanceof Element)) {
|
|
3077
3430
|
throw { code: 'E_NOT_FOUND', message: `table not found: ${String(params.table ?? '')}` } satisfies ActionError;
|
|
3078
3431
|
}
|
|
3079
|
-
const
|
|
3080
|
-
|
|
3081
|
-
? { columns: htmlTableSchema(resolved.element) }
|
|
3082
|
-
: { columns: gridSchema(resolved.element) };
|
|
3432
|
+
const table = withTableIntelligence(resolved.table, resolved.element);
|
|
3433
|
+
const schema = { columns: tableSchemaFromElement(resolved.element) };
|
|
3083
3434
|
return {
|
|
3084
|
-
table
|
|
3435
|
+
table,
|
|
3085
3436
|
schema
|
|
3086
3437
|
};
|
|
3087
3438
|
}
|
|
@@ -3099,15 +3450,32 @@ async function dispatchRpc(method: string, params: Record<string, unknown> = {})
|
|
|
3099
3450
|
: typeof params.limit === 'number'
|
|
3100
3451
|
? Math.max(1, Math.floor(params.limit))
|
|
3101
3452
|
: 100;
|
|
3102
|
-
const
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
resolved.element instanceof HTMLTableElement
|
|
3106
|
-
|
|
3107
|
-
|
|
3453
|
+
const table = withTableIntelligence(resolved.table, resolved.element);
|
|
3454
|
+
let rows = tableRowsFromElement(resolved.element, requestedLimit);
|
|
3455
|
+
let extractionMode: TableExtractionMetadata['mode'] =
|
|
3456
|
+
table.intelligence?.preferredExtractionMode ?? (resolved.element instanceof HTMLTableElement ? 'dataSource' : 'visibleOnly');
|
|
3457
|
+
let extraction = buildExtractionMetadata(
|
|
3458
|
+
extractionMode,
|
|
3459
|
+
rows,
|
|
3460
|
+
table.intelligence,
|
|
3461
|
+
params.all === true && extractionMode === 'visibleOnly'
|
|
3462
|
+
? ['Only visible rows could be read from this table region.']
|
|
3463
|
+
: [],
|
|
3464
|
+
{
|
|
3465
|
+
reachedEnd: extractionMode === 'dataSource',
|
|
3466
|
+
limitApplied: params.all === true && rows.length >= requestedLimit
|
|
3467
|
+
}
|
|
3468
|
+
);
|
|
3469
|
+
if (params.all === true && extractionMode === 'scroll') {
|
|
3470
|
+
const scrolled = await scrollExtractRows(resolved.element, requestedLimit, table.intelligence);
|
|
3471
|
+
rows = scrolled.rows;
|
|
3472
|
+
extractionMode = scrolled.extractionMode;
|
|
3473
|
+
extraction = scrolled.extraction;
|
|
3474
|
+
}
|
|
3108
3475
|
return {
|
|
3109
|
-
table
|
|
3476
|
+
table,
|
|
3110
3477
|
extractionMode,
|
|
3478
|
+
extraction,
|
|
3111
3479
|
rows
|
|
3112
3480
|
};
|
|
3113
3481
|
}
|