@tita-agent/dsh-data-table 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +172 -2
- package/cordis.patch.yml +7 -0
- package/lib/client.js +16766 -0
- package/lib/contract-envelope.d.ts +91 -0
- package/lib/contract-envelope.js +106 -0
- package/lib/contract-layout.d.ts +130 -0
- package/lib/contract-layout.js +138 -0
- package/lib/contract-refs.d.ts +39 -0
- package/lib/contract-refs.js +9 -0
- package/lib/contract-version.d.ts +311 -0
- package/lib/contract-version.js +270 -0
- package/lib/contract-wire.d.ts +147 -0
- package/lib/contract-wire.js +99 -0
- package/lib/contracts.d.ts +12 -0
- package/lib/contracts.js +12 -0
- package/lib/types/audit.d.ts +42 -0
- package/lib/types/audit.js +39 -0
- package/lib/types/client/ProjectImportMenu.d.ts +30 -0
- package/lib/types/client/ProjectImportMenu.js +70 -0
- package/lib/types/client/SheetPanel.d.ts +33 -0
- package/lib/types/client/SheetPanel.js +1766 -0
- package/lib/types/client/SheetToggle.d.ts +18 -0
- package/lib/types/client/SheetToggle.js +26 -0
- package/lib/types/client/index.d.ts +15 -0
- package/lib/types/client/index.js +110 -0
- package/lib/types/client/persistence/changes.d.ts +15 -0
- package/lib/types/client/persistence/changes.js +51 -0
- package/lib/types/client/persistence/ids.d.ts +34 -0
- package/lib/types/client/persistence/ids.js +48 -0
- package/lib/types/client/persistence/merge.d.ts +35 -0
- package/lib/types/client/persistence/merge.js +362 -0
- package/lib/types/client/store.d.ts +61 -0
- package/lib/types/client/store.js +152 -0
- package/lib/types/client/surface-context.d.ts +27 -0
- package/lib/types/client/surface-context.js +104 -0
- package/lib/types/client/surface-control.d.ts +2 -0
- package/lib/types/client/surface-control.js +12 -0
- package/lib/types/client/transport/workbookApi.d.ts +159 -0
- package/lib/types/client/transport/workbookApi.js +246 -0
- package/lib/types/client/univer/StandaloneWorkbookPage.d.ts +5 -0
- package/lib/types/client/univer/StandaloneWorkbookPage.js +7 -0
- package/lib/types/client/univer/WorkbookCanvas.d.ts +40 -0
- package/lib/types/client/univer/WorkbookCanvas.js +87 -0
- package/lib/types/client/univer/WorkbookView.d.ts +3 -0
- package/lib/types/client/univer/WorkbookView.js +2 -0
- package/lib/types/client/univer/loader.d.ts +18 -0
- package/lib/types/client/univer/loader.js +81 -0
- package/lib/types/client/univer/standalone-entry.d.ts +1 -0
- package/lib/types/client/univer/standalone-entry.js +19 -0
- package/lib/types/client/versions/VersionChrome.d.ts +33 -0
- package/lib/types/client/versions/VersionChrome.js +21 -0
- package/lib/types/client/versions/inspect.d.ts +70 -0
- package/lib/types/client/versions/inspect.js +88 -0
- package/lib/types/client/versions/preview-decoration.d.ts +11 -0
- package/lib/types/client/versions/preview-decoration.js +54 -0
- package/lib/types/client/versions/records.d.ts +19 -0
- package/lib/types/client/versions/records.js +70 -0
- package/lib/types/client/workspace-host.d.ts +61 -0
- package/lib/types/client/workspace-host.js +124 -0
- package/lib/types/collaboration-hub.d.ts +31 -0
- package/lib/types/collaboration-hub.js +69 -0
- package/lib/types/contract/ids.d.ts +24 -0
- package/lib/types/contract/ids.js +46 -0
- package/lib/types/contract/mutations.d.ts +25 -0
- package/lib/types/contract/mutations.js +782 -0
- package/lib/types/contract/operations.d.ts +54 -0
- package/lib/types/contract/operations.js +203 -0
- package/lib/types/contract/snapshots.d.ts +176 -0
- package/lib/types/contract/snapshots.js +351 -0
- package/lib/types/contract/univer-api-catalog.d.ts +29 -0
- package/lib/types/contract/univer-api-catalog.js +264 -0
- package/lib/types/errors.d.ts +8 -0
- package/lib/types/errors.js +12 -0
- package/lib/types/http/assets-routes.d.ts +3 -0
- package/lib/types/http/assets-routes.js +169 -0
- package/lib/types/http/collaboration.d.ts +10 -0
- package/lib/types/http/collaboration.js +154 -0
- package/lib/types/http/history.d.ts +15 -0
- package/lib/types/http/history.js +134 -0
- package/lib/types/http/project-files.d.ts +79 -0
- package/lib/types/http/project-files.js +206 -0
- package/lib/types/http/project-scope.d.ts +50 -0
- package/lib/types/http/project-scope.js +52 -0
- package/lib/types/http/routes.d.ts +98 -0
- package/lib/types/http/routes.js +323 -0
- package/lib/types/http/standalone.d.ts +9 -0
- package/lib/types/http/standalone.js +31 -0
- package/lib/types/http/trunk.d.ts +10 -0
- package/lib/types/http/trunk.js +322 -0
- package/lib/types/http/worktree.d.ts +9 -0
- package/lib/types/http/worktree.js +145 -0
- package/lib/types/index.d.ts +18 -0
- package/lib/types/index.js +156 -0
- package/lib/types/invariant.d.ts +15 -0
- package/lib/types/invariant.js +15 -0
- package/lib/types/request-params.d.ts +21 -0
- package/lib/types/request-params.js +76 -0
- package/lib/types/store/paths.d.ts +68 -0
- package/lib/types/store/paths.js +148 -0
- package/lib/types/store/project.d.ts +88 -0
- package/lib/types/store/project.js +120 -0
- package/lib/types/store/read.d.ts +110 -0
- package/lib/types/store/read.js +404 -0
- package/lib/types/store/retention.d.ts +27 -0
- package/lib/types/store/retention.js +111 -0
- package/lib/types/store/write.d.ts +22 -0
- package/lib/types/store/write.js +210 -0
- package/lib/types/tools/arguments.d.ts +40 -0
- package/lib/types/tools/arguments.js +130 -0
- package/lib/types/tools/cells.d.ts +50 -0
- package/lib/types/tools/cells.js +298 -0
- package/lib/types/tools/index.d.ts +2 -0
- package/lib/types/tools/index.js +250 -0
- package/lib/types/tools/render.d.ts +47 -0
- package/lib/types/tools/render.js +290 -0
- package/lib/types/tools/univer-api.d.ts +18 -0
- package/lib/types/tools/univer-api.js +198 -0
- package/lib/types/tools/workbook-catalog.d.ts +3 -0
- package/lib/types/tools/workbook-catalog.js +83 -0
- package/lib/types/tools/workbook-edit.d.ts +192 -0
- package/lib/types/tools/workbook-edit.js +912 -0
- package/lib/types/tools/workbook-find.d.ts +11 -0
- package/lib/types/tools/workbook-find.js +89 -0
- package/lib/types/tools/workbook-history.d.ts +2 -0
- package/lib/types/tools/workbook-history.js +59 -0
- package/lib/types/tools/workbook-read.d.ts +21 -0
- package/lib/types/tools/workbook-read.js +204 -0
- package/lib/types/types.d.ts +377 -0
- package/lib/types/types.js +34 -0
- package/lib/types/univer/apply-operation.d.ts +14 -0
- package/lib/types/univer/apply-operation.js +398 -0
- package/lib/types/univer/chunk.d.ts +68 -0
- package/lib/types/univer/chunk.js +16 -0
- package/lib/types/univer/decoration.d.ts +34 -0
- package/lib/types/univer/decoration.js +88 -0
- package/lib/types/univer/diagnostics.d.ts +31 -0
- package/lib/types/univer/diagnostics.js +141 -0
- package/lib/types/univer/engine.d.ts +33 -0
- package/lib/types/univer/engine.js +338 -0
- package/lib/types/univer/entry.d.ts +8 -0
- package/lib/types/univer/entry.js +11 -0
- package/lib/types/univer/facade.d.ts +107 -0
- package/lib/types/univer/facade.js +1 -0
- package/lib/types/univer/fonts.d.ts +202 -0
- package/lib/types/univer/fonts.js +140 -0
- package/lib/types/univer/plugins/collaboration.d.ts +23 -0
- package/lib/types/univer/plugins/collaboration.js +47 -0
- package/lib/types/univer/plugins/export.d.ts +39 -0
- package/lib/types/univer/plugins/export.js +246 -0
- package/lib/types/univer/plugins/history.d.ts +24 -0
- package/lib/types/univer/plugins/history.js +50 -0
- package/lib/types/univer/plugins/version.d.ts +13 -0
- package/lib/types/univer/plugins/version.js +13 -0
- package/lib/types/version/diff.d.ts +36 -0
- package/lib/types/version/diff.js +156 -0
- package/lib/types/version/grouping.d.ts +7 -0
- package/lib/types/version/grouping.js +94 -0
- package/lib/types/version/records.d.ts +17 -0
- package/lib/types/version/records.js +102 -0
- package/lib/types/version/session.d.ts +36 -0
- package/lib/types/version/session.js +89 -0
- package/lib/types/version/summary.d.ts +24 -0
- package/lib/types/version/summary.js +111 -0
- package/lib/types/workbook-manager.d.ts +191 -0
- package/lib/types/workbook-manager.js +935 -0
- package/lib/types/xlsx-export-worker.d.ts +5 -0
- package/lib/types/xlsx-export-worker.js +24 -0
- package/lib/types/xlsx-export.d.ts +21 -0
- package/lib/types/xlsx-export.js +371 -0
- package/lib/types/xlsx-import-worker.d.ts +1 -0
- package/lib/types/xlsx-import-worker.js +19 -0
- package/lib/univer-client.js +690 -0
- package/lib/workbook-page.js +23826 -0
- package/lib/xlsx-export-worker.js +104 -0
- package/lib/xlsx-import-worker.js +46 -0
- package/package.json +106 -3
- package/src/audit.ts +58 -0
- package/src/client/ProjectImportMenu.tsx +100 -0
- package/src/client/SheetPanel.module.css +1578 -0
- package/src/client/SheetPanel.tsx +1934 -0
- package/src/client/SheetToggle.module.css +73 -0
- package/src/client/SheetToggle.tsx +65 -0
- package/src/client/contract.d.ts +19 -0
- package/src/client/index.ts +131 -0
- package/src/client/persistence/changes.ts +58 -0
- package/src/client/persistence/ids.ts +60 -0
- package/src/client/persistence/merge.ts +392 -0
- package/src/client/store.ts +188 -0
- package/src/client/surface-context.ts +123 -0
- package/src/client/surface-control.ts +13 -0
- package/src/client/transport/workbookApi.ts +355 -0
- package/src/client/univer/SheetView.module.css +110 -0
- package/src/client/univer/StandaloneWorkbookPage.module.css +8 -0
- package/src/client/univer/StandaloneWorkbookPage.tsx +15 -0
- package/src/client/univer/WorkbookCanvas.tsx +128 -0
- package/src/client/univer/WorkbookView.module.css +30 -0
- package/src/client/univer/WorkbookView.tsx +3 -0
- package/src/client/univer/loader.ts +90 -0
- package/src/client/univer/react-dom-client.d.ts +10 -0
- package/src/client/univer/standalone-entry.tsx +19 -0
- package/src/client/univer/standalone.css +12 -0
- package/src/client/univer/univer-locales.d.ts +89 -0
- package/src/client/univer/univer-modules.d.ts +26 -0
- package/src/client/versions/VersionChrome.tsx +114 -0
- package/src/client/versions/inspect.ts +138 -0
- package/src/client/versions/preview-decoration.ts +54 -0
- package/src/client/versions/records.ts +78 -0
- package/src/client/workspace-host.ts +147 -0
- package/src/collaboration-hub.ts +83 -0
- package/src/contract/ids.ts +50 -0
- package/src/contract/mutations.ts +791 -0
- package/src/contract/operations.ts +221 -0
- package/src/contract/snapshots.ts +383 -0
- package/src/contract/univer-api-catalog.ts +289 -0
- package/src/css-modules.d.ts +6 -0
- package/src/errors.ts +25 -0
- package/src/http/assets-routes.ts +177 -0
- package/src/http/collaboration.ts +144 -0
- package/src/http/history.ts +151 -0
- package/src/http/project-files.ts +227 -0
- package/src/http/project-scope.ts +77 -0
- package/src/http/routes.ts +364 -0
- package/src/http/standalone.ts +32 -0
- package/src/http/trunk.ts +322 -0
- package/src/http/worktree.ts +119 -0
- package/src/index.ts +165 -0
- package/src/invariant.ts +17 -0
- package/src/request-params.ts +92 -0
- package/src/store/paths.ts +165 -0
- package/src/store/project.ts +137 -0
- package/src/store/read.ts +454 -0
- package/src/store/retention.ts +106 -0
- package/src/store/write.ts +230 -0
- package/src/tools/arguments.ts +128 -0
- package/src/tools/cells.ts +298 -0
- package/src/tools/index.ts +274 -0
- package/src/tools/render.ts +301 -0
- package/src/tools/univer-api.ts +243 -0
- package/src/tools/workbook-catalog.ts +94 -0
- package/src/tools/workbook-edit.ts +987 -0
- package/src/tools/workbook-find.ts +80 -0
- package/src/tools/workbook-history.ts +63 -0
- package/src/tools/workbook-read.ts +210 -0
- package/src/types.ts +432 -0
- package/src/univer/apply-operation.ts +369 -0
- package/src/univer/chunk.ts +77 -0
- package/src/univer/decoration.ts +111 -0
- package/src/univer/diagnostics.ts +136 -0
- package/src/univer/engine.ts +370 -0
- package/src/univer/entry.ts +14 -0
- package/src/univer/facade.ts +91 -0
- package/src/univer/fonts.ts +148 -0
- package/src/univer/overrides.css +33 -0
- package/src/univer/plugins/collaboration.ts +47 -0
- package/src/univer/plugins/export.ts +236 -0
- package/src/univer/plugins/history.ts +49 -0
- package/src/univer/plugins/version.ts +13 -0
- package/src/version/diff.ts +169 -0
- package/src/version/grouping.ts +96 -0
- package/src/version/records.ts +113 -0
- package/src/version/session.ts +90 -0
- package/src/version/summary.ts +117 -0
- package/src/workbook-manager.ts +1111 -0
- package/src/xlsx-export-worker.ts +34 -0
- package/src/xlsx-export.ts +373 -0
- package/src/xlsx-import-worker.ts +30 -0
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The snapshot mutator: one DSH operation in, the rewritten snapshot out
|
|
3
|
+
* (docs/issue/0022 P0/P1).
|
|
4
|
+
*
|
|
5
|
+
* Pure and Univer-free — it is the model's own answer to "what does this
|
|
6
|
+
* operation mean", and `store/read.ts` replays operations through it when a
|
|
7
|
+
* workbook's snapshot is older than its revision. It lives in `contract/`
|
|
8
|
+
* rather than in `store/` precisely because it is not about the disk: moving
|
|
9
|
+
* the read path into the store without it would have made `store/` import
|
|
10
|
+
* *up* into `workbook-manager`, which the architecture guard forbids.
|
|
11
|
+
*/
|
|
12
|
+
import { SHEET_MAX_COLUMNS, SHEET_MAX_PHYSICAL_ROWS, SHEET_MAX_ROWS, } from "../types.js";
|
|
13
|
+
import { randomUUID } from 'node:crypto';
|
|
14
|
+
import { asWorkbookSnapshot, cloneSnapshot, findDuplicateRows, isSheetSchema, resolveSheet, toCellValue, validateIndex, validateRange, DEFAULT_SHEET_ROWS, DEFAULT_SHEET_COLUMNS, normalizeCellFormula, } from "./snapshots.js";
|
|
15
|
+
import { APPEND_HEADER_MISMATCH } from "./operations.js";
|
|
16
|
+
/** Apply one DSH operation to a Univer-compatible snapshot. */
|
|
17
|
+
export function applyWorkbookOperation(snapshot, operation) {
|
|
18
|
+
if (operation.type === 'snapshot_replace') {
|
|
19
|
+
validateSnapshotBounds(operation.payload.snapshot);
|
|
20
|
+
// A copy, not the payload itself. The replay loop in `store/read.ts` keeps
|
|
21
|
+
// applying later operations to whatever this returns, so handing back the
|
|
22
|
+
// caller's object makes the *stored* operation payload accumulate every
|
|
23
|
+
// later revision — and `store/write.ts` persists `meta.operations` verbatim,
|
|
24
|
+
// so a rollback or step diff for this revision would show a later state.
|
|
25
|
+
// The neighbouring branches clone for the same reason; this one did not.
|
|
26
|
+
return cloneSnapshot(asWorkbookSnapshot(operation.payload.snapshot));
|
|
27
|
+
}
|
|
28
|
+
const root = asWorkbookSnapshot(snapshot);
|
|
29
|
+
const payload = operation.payload;
|
|
30
|
+
switch (operation.type) {
|
|
31
|
+
case 'update_cells':
|
|
32
|
+
updateCells(root, payload.sheetId, payload.cells ?? []);
|
|
33
|
+
break;
|
|
34
|
+
case 'clear_range':
|
|
35
|
+
clearRange(root, payload.sheetId, payload.range);
|
|
36
|
+
break;
|
|
37
|
+
case 'delete_cells':
|
|
38
|
+
deleteCells(root, payload.sheetId, payload.range, payload.shift ?? 'clear');
|
|
39
|
+
break;
|
|
40
|
+
case 'insert_rows':
|
|
41
|
+
insertRows(root, payload.sheetId, payload.range?.startRow ?? 0, payload.count ?? 1);
|
|
42
|
+
break;
|
|
43
|
+
case 'delete_rows':
|
|
44
|
+
deleteRows(root, payload.sheetId, payload.range?.startRow ?? 0, payload.count ?? 1);
|
|
45
|
+
break;
|
|
46
|
+
case 'insert_columns':
|
|
47
|
+
insertColumns(root, payload.sheetId, payload.range?.startColumn ?? 0, payload.count ?? 1);
|
|
48
|
+
break;
|
|
49
|
+
case 'delete_columns':
|
|
50
|
+
deleteColumns(root, payload.sheetId, payload.range?.startColumn ?? 0, payload.count ?? 1);
|
|
51
|
+
break;
|
|
52
|
+
case 'create_sheet':
|
|
53
|
+
createSheet(root, payload.sheetId ?? `sheet-${operation.opId.slice(0, 12)}`, payload.name);
|
|
54
|
+
break;
|
|
55
|
+
case 'delete_sheet':
|
|
56
|
+
deleteSheet(root, payload.sheetId ?? payload.sheetName);
|
|
57
|
+
break;
|
|
58
|
+
case 'rename_sheet':
|
|
59
|
+
renameSheet(root, payload.sheetId, payload.name);
|
|
60
|
+
break;
|
|
61
|
+
case 'append_rows':
|
|
62
|
+
appendRows(root, payload.sheetId ?? payload.sheetName, payload.columns ?? [], payload.rows ?? [], payload.meta, payload.name, payload.headerRowIndex, payload.verifyHeader);
|
|
63
|
+
break;
|
|
64
|
+
case 'format_cells':
|
|
65
|
+
formatCells(root, payload.sheetId ?? payload.sheetName, payload.range, payload.style);
|
|
66
|
+
break;
|
|
67
|
+
case 'merge_cells':
|
|
68
|
+
mergeCells(root, payload.sheetId ?? payload.sheetName, payload.range ?? payload.merge);
|
|
69
|
+
break;
|
|
70
|
+
case 'unmerge_cells':
|
|
71
|
+
unmergeCells(root, payload.sheetId ?? payload.sheetName, payload.range ?? payload.merge);
|
|
72
|
+
break;
|
|
73
|
+
case 'freeze_panes':
|
|
74
|
+
freezePanes(root, payload.sheetId ?? payload.sheetName, payload.freeze);
|
|
75
|
+
break;
|
|
76
|
+
case 'set_column_width':
|
|
77
|
+
setDimension(root, payload.sheetId ?? payload.sheetName, 'columnData', payload.range?.startColumn ?? 0, payload.size);
|
|
78
|
+
break;
|
|
79
|
+
case 'set_row_height':
|
|
80
|
+
setDimension(root, payload.sheetId ?? payload.sheetName, 'rowData', payload.range?.startRow ?? 0, payload.size);
|
|
81
|
+
break;
|
|
82
|
+
case 'sort_range':
|
|
83
|
+
sortRange(root, payload.sheetId ?? payload.sheetName, payload.range, payload.sort);
|
|
84
|
+
break;
|
|
85
|
+
case 'set_hyperlink':
|
|
86
|
+
setHyperlink(root, payload.sheetId ?? payload.sheetName, payload.range, payload.url, payload.label);
|
|
87
|
+
break;
|
|
88
|
+
case 'remove_hyperlink':
|
|
89
|
+
removeHyperlink(root, payload.sheetId ?? payload.sheetName, payload.range);
|
|
90
|
+
break;
|
|
91
|
+
case 'remove_duplicates':
|
|
92
|
+
removeDuplicateRows(root, payload.sheetId ?? payload.sheetName, payload);
|
|
93
|
+
break;
|
|
94
|
+
case 'batch':
|
|
95
|
+
for (const [index, child] of (payload.operations ?? []).entries()) {
|
|
96
|
+
applyWorkbookOperation(root, {
|
|
97
|
+
opId: `${operation.opId}:${index}`,
|
|
98
|
+
workbookId: operation.workbookId,
|
|
99
|
+
revision: operation.revision,
|
|
100
|
+
actor: operation.actor,
|
|
101
|
+
timestamp: operation.timestamp,
|
|
102
|
+
type: child.type,
|
|
103
|
+
payload: child.payload,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
default:
|
|
108
|
+
assertNeverOperation(operation.type);
|
|
109
|
+
}
|
|
110
|
+
return root;
|
|
111
|
+
}
|
|
112
|
+
/** Read a rectangular range as a compact 2D value matrix. */
|
|
113
|
+
export function validateSnapshotBounds(value) {
|
|
114
|
+
const serialized = JSON.stringify(value);
|
|
115
|
+
if (serialized === undefined || serialized.length > 25 * 1024 * 1024)
|
|
116
|
+
throw new Error('工作簿快照过大(最大 25 MB)');
|
|
117
|
+
const root = asWorkbookSnapshot(value);
|
|
118
|
+
if (root.sheetOrder.length > 100 || Object.keys(root.sheets).length > 100)
|
|
119
|
+
throw new Error('工作簿 sheet 数量超过上限(最多 100 个)');
|
|
120
|
+
const seenSheetIds = new Set();
|
|
121
|
+
for (const sheetId of root.sheetOrder) {
|
|
122
|
+
if (seenSheetIds.has(sheetId) || root.sheets[sheetId] === undefined)
|
|
123
|
+
throw new Error('工作簿 sheet 顺序无效');
|
|
124
|
+
seenSheetIds.add(sheetId);
|
|
125
|
+
}
|
|
126
|
+
for (const [sheetId, sheet] of Object.entries(root.sheets)) {
|
|
127
|
+
if (typeof sheet !== 'object' || sheet === null)
|
|
128
|
+
throw new Error('工作簿 sheet 格式无效');
|
|
129
|
+
if (sheet.id !== sheetId || typeof sheet.name !== 'string' || sheet.name.trim().length === 0 || sheet.name.length > 64)
|
|
130
|
+
throw new Error('工作簿 sheet 元数据无效');
|
|
131
|
+
if (typeof sheet.cellData !== 'object' || sheet.cellData === null || Array.isArray(sheet.cellData))
|
|
132
|
+
throw new Error('工作簿 cellData 格式无效');
|
|
133
|
+
const rowCount = Number(sheet.rowCount);
|
|
134
|
+
const columnCount = Number(sheet.columnCount);
|
|
135
|
+
if (!Number.isInteger(rowCount) || rowCount < 1 || rowCount > SHEET_MAX_PHYSICAL_ROWS)
|
|
136
|
+
throw new Error('工作簿行数超过上限');
|
|
137
|
+
if (!Number.isInteger(columnCount) || columnCount < 1 || columnCount > SHEET_MAX_COLUMNS)
|
|
138
|
+
throw new Error('工作簿列数超过上限');
|
|
139
|
+
for (const [rowKey, row] of Object.entries(sheet.cellData)) {
|
|
140
|
+
const rowIndex = Number(rowKey);
|
|
141
|
+
if (!Number.isInteger(rowIndex) || rowIndex < 0 || rowIndex >= SHEET_MAX_PHYSICAL_ROWS)
|
|
142
|
+
throw new Error('工作簿包含越界行');
|
|
143
|
+
if (typeof row !== 'object' || row === null || Array.isArray(row))
|
|
144
|
+
throw new Error('工作簿行数据格式无效');
|
|
145
|
+
for (const columnKey of Object.keys(row)) {
|
|
146
|
+
const columnIndex = Number(columnKey);
|
|
147
|
+
if (!Number.isInteger(columnIndex) || columnIndex < 0 || columnIndex >= SHEET_MAX_COLUMNS)
|
|
148
|
+
throw new Error('工作簿包含越界列');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Guard operations whose full Excel semantics are not represented by the
|
|
154
|
+
* compact Host snapshot mutator. A snapshot fallback remains available for
|
|
155
|
+
* these workbooks through import/user edits. */
|
|
156
|
+
function assertStructuralOperationSupported(root, sheet, operation) {
|
|
157
|
+
const hasFormula = Object.values(root.sheets).some(candidate => Object.values(candidate.cellData ?? {}).some(row => Object.values(row).some(cell => typeof cell.f === 'string' && cell.f.length > 0)));
|
|
158
|
+
const hasAdvancedState = ['mergeData', 'rowData', 'columnData', 'freeze', 'conditionalFormatting', 'dataValidation', 'drawings', 'tables', 'comments'].some(key => {
|
|
159
|
+
const value = sheet[key];
|
|
160
|
+
if (value === undefined || value === null)
|
|
161
|
+
return false;
|
|
162
|
+
if (Array.isArray(value))
|
|
163
|
+
return value.length > 0;
|
|
164
|
+
if (typeof value === 'object')
|
|
165
|
+
return Object.keys(value).length > 0;
|
|
166
|
+
return true;
|
|
167
|
+
});
|
|
168
|
+
if (hasFormula || hasAdvancedState) {
|
|
169
|
+
throw new Error(`${operation} 在含公式或高级结构的 sheet 上被拒绝,以避免破坏引用和格式;写更多行请直接用 update_cells 或 import_file(越界会自动扩容 rowCount,不必先 insert_rows),整体替换请改用 snapshot`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function maxCellIndex(cellData, dimension) {
|
|
173
|
+
let max = -1;
|
|
174
|
+
if (dimension === 'row') {
|
|
175
|
+
for (const key of Object.keys(cellData)) {
|
|
176
|
+
const value = Number(key);
|
|
177
|
+
if (Number.isInteger(value))
|
|
178
|
+
max = Math.max(max, value);
|
|
179
|
+
}
|
|
180
|
+
return max;
|
|
181
|
+
}
|
|
182
|
+
for (const row of Object.values(cellData)) {
|
|
183
|
+
for (const key of Object.keys(row)) {
|
|
184
|
+
const value = Number(key);
|
|
185
|
+
if (Number.isInteger(value))
|
|
186
|
+
max = Math.max(max, value);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return max;
|
|
190
|
+
}
|
|
191
|
+
function recomputeDataSheetStats(sheet) {
|
|
192
|
+
let maxDataRow = 0;
|
|
193
|
+
let maxDataColumn = 0;
|
|
194
|
+
for (const [rowKey, row] of Object.entries(sheet.cellData ?? {})) {
|
|
195
|
+
const rowIndex = Number(rowKey);
|
|
196
|
+
if (!Number.isInteger(rowIndex))
|
|
197
|
+
continue;
|
|
198
|
+
if (rowIndex > 0 && Object.keys(row).length > 0)
|
|
199
|
+
maxDataRow = Math.max(maxDataRow, rowIndex);
|
|
200
|
+
for (const columnKey of Object.keys(row)) {
|
|
201
|
+
const columnIndex = Number(columnKey);
|
|
202
|
+
if (Number.isInteger(columnIndex))
|
|
203
|
+
maxDataColumn = Math.max(maxDataColumn, columnIndex + 1);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Recompute from the resulting cell map so clearing the last populated cell
|
|
207
|
+
// cannot leave a stale row/column count in the schema and tool output.
|
|
208
|
+
sheet.dataSheetStats = { rowCount: maxDataRow, columnCount: maxDataColumn };
|
|
209
|
+
const schema = isSheetSchema(sheet.dataSheetSchema) ? sheet.dataSheetSchema : undefined;
|
|
210
|
+
if (schema !== undefined) {
|
|
211
|
+
sheet.dataSheetSchema = { ...schema, rowCount: maxDataRow, updatedAt: new Date().toISOString() };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function updateCells(root, sheetRef, cells) {
|
|
215
|
+
if (cells.length === 0)
|
|
216
|
+
throw new Error('cells 不能为空');
|
|
217
|
+
if (cells.length > SHEET_MAX_ROWS * SHEET_MAX_COLUMNS)
|
|
218
|
+
throw new Error('一次修改的单元格数量超过上限');
|
|
219
|
+
const sheet = resolveSheet(root, sheetRef);
|
|
220
|
+
for (const cell of cells) {
|
|
221
|
+
validateIndex(cell.row, 'cell.row');
|
|
222
|
+
validateIndex(cell.column, 'cell.column');
|
|
223
|
+
if (cell.row >= SHEET_MAX_PHYSICAL_ROWS)
|
|
224
|
+
throw new Error(`cell.row 超过上限(最多 ${SHEET_MAX_ROWS} 条数据行 + 表头)`);
|
|
225
|
+
if (cell.column >= SHEET_MAX_COLUMNS)
|
|
226
|
+
throw new Error(`cell.column 超过上限(最多 ${SHEET_MAX_COLUMNS} 列)`);
|
|
227
|
+
const row = (sheet.cellData[String(cell.row)] ??= {});
|
|
228
|
+
const formula = normalizeCellFormula(cell.formula);
|
|
229
|
+
if (cell.value === null && formula === undefined) {
|
|
230
|
+
delete row[String(cell.column)];
|
|
231
|
+
if (Object.keys(row).length === 0)
|
|
232
|
+
delete sheet.cellData[String(cell.row)];
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const current = row[String(cell.column)];
|
|
236
|
+
row[String(cell.column)] = formula === undefined
|
|
237
|
+
? { ...(current ?? {}), v: cell.value, f: undefined }
|
|
238
|
+
: { ...(current ?? {}), v: cell.value, f: formula };
|
|
239
|
+
if (formula === undefined)
|
|
240
|
+
delete row[String(cell.column)].f;
|
|
241
|
+
sheet.rowCount = Math.max(sheet.rowCount, cell.row + 1);
|
|
242
|
+
sheet.columnCount = Math.max(sheet.columnCount, cell.column + 1);
|
|
243
|
+
}
|
|
244
|
+
recomputeDataSheetStats(sheet);
|
|
245
|
+
}
|
|
246
|
+
function clearRange(root, sheetRef, range) {
|
|
247
|
+
validateRange(range);
|
|
248
|
+
const sheet = resolveSheet(root, sheetRef);
|
|
249
|
+
for (let row = range.startRow; row <= range.endRow; row += 1) {
|
|
250
|
+
const cells = sheet.cellData[String(row)];
|
|
251
|
+
if (cells === undefined)
|
|
252
|
+
continue;
|
|
253
|
+
for (let column = range.startColumn; column <= range.endColumn; column += 1)
|
|
254
|
+
delete cells[String(column)];
|
|
255
|
+
if (Object.keys(cells).length === 0)
|
|
256
|
+
delete sheet.cellData[String(row)];
|
|
257
|
+
}
|
|
258
|
+
recomputeDataSheetStats(sheet);
|
|
259
|
+
}
|
|
260
|
+
function deleteCells(root, sheetRef, range, shift) {
|
|
261
|
+
validateRange(range);
|
|
262
|
+
if (shift !== 'clear' && shift !== 'left' && shift !== 'up') {
|
|
263
|
+
throw new Error(`不支持的单元格删除方式:${String(shift)}`);
|
|
264
|
+
}
|
|
265
|
+
if (shift === 'clear') {
|
|
266
|
+
clearRange(root, sheetRef, range);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const sheet = resolveSheet(root, sheetRef);
|
|
270
|
+
assertStructuralOperationSupported(root, sheet, `delete_cells:${shift}`);
|
|
271
|
+
const width = range.endColumn - range.startColumn + 1;
|
|
272
|
+
const height = range.endRow - range.startRow + 1;
|
|
273
|
+
const nextCellData = {};
|
|
274
|
+
for (const [rowKey, cells] of Object.entries(sheet.cellData)) {
|
|
275
|
+
const row = Number(rowKey);
|
|
276
|
+
if (!Number.isInteger(row))
|
|
277
|
+
continue;
|
|
278
|
+
const nextCells = {};
|
|
279
|
+
for (const [columnKey, cell] of Object.entries(cells)) {
|
|
280
|
+
const column = Number(columnKey);
|
|
281
|
+
if (!Number.isInteger(column))
|
|
282
|
+
continue;
|
|
283
|
+
if (shift === 'left' && row >= range.startRow && row <= range.endRow) {
|
|
284
|
+
if (column >= range.startColumn && column <= range.endColumn)
|
|
285
|
+
continue;
|
|
286
|
+
nextCells[String(column > range.endColumn ? column - width : column)] = cell;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (shift === 'up' && column >= range.startColumn && column <= range.endColumn) {
|
|
290
|
+
if (row >= range.startRow && row <= range.endRow)
|
|
291
|
+
continue;
|
|
292
|
+
nextCellData[String(row > range.endRow ? row - height : row)] ??= {};
|
|
293
|
+
nextCellData[String(row > range.endRow ? row - height : row)][columnKey] = cell;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
nextCells[columnKey] = cell;
|
|
297
|
+
}
|
|
298
|
+
if (Object.keys(nextCells).length > 0) {
|
|
299
|
+
nextCellData[rowKey] ??= {};
|
|
300
|
+
Object.assign(nextCellData[rowKey], nextCells);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
sheet.cellData = nextCellData;
|
|
304
|
+
recomputeDataSheetStats(sheet);
|
|
305
|
+
}
|
|
306
|
+
function insertRows(root, sheetRef, startRow, count) {
|
|
307
|
+
validateIndex(startRow, 'startRow');
|
|
308
|
+
validateCount(count);
|
|
309
|
+
const sheet = resolveSheet(root, sheetRef);
|
|
310
|
+
if (sheet.rowCount + count > SHEET_MAX_PHYSICAL_ROWS)
|
|
311
|
+
throw new Error(`插入后行数超过上限(最多 ${SHEET_MAX_ROWS} 条数据行 + 表头)`);
|
|
312
|
+
assertStructuralOperationSupported(root, sheet, 'insert_rows');
|
|
313
|
+
if (maxCellIndex(sheet.cellData, 'row') + count >= SHEET_MAX_PHYSICAL_ROWS)
|
|
314
|
+
throw new Error('插入后单元格超出工作簿行上限');
|
|
315
|
+
const rows = Object.keys(sheet.cellData).map(Number).filter(Number.isInteger).sort((a, b) => b - a);
|
|
316
|
+
for (const row of rows) {
|
|
317
|
+
if (row < startRow)
|
|
318
|
+
continue;
|
|
319
|
+
sheet.cellData[String(row + count)] = sheet.cellData[String(row)];
|
|
320
|
+
delete sheet.cellData[String(row)];
|
|
321
|
+
}
|
|
322
|
+
sheet.rowCount += count;
|
|
323
|
+
recomputeDataSheetStats(sheet);
|
|
324
|
+
}
|
|
325
|
+
function deleteRows(root, sheetRef, startRow, count) {
|
|
326
|
+
validateIndex(startRow, 'startRow');
|
|
327
|
+
validateCount(count);
|
|
328
|
+
if (startRow >= SHEET_MAX_PHYSICAL_ROWS)
|
|
329
|
+
throw new Error(`startRow 超过上限(最多 ${SHEET_MAX_ROWS} 条数据行 + 表头)`);
|
|
330
|
+
if (startRow + count > SHEET_MAX_PHYSICAL_ROWS)
|
|
331
|
+
throw new Error(`删除范围超过上限(最多 ${SHEET_MAX_ROWS} 条数据行 + 表头)`);
|
|
332
|
+
const sheet = resolveSheet(root, sheetRef);
|
|
333
|
+
assertStructuralOperationSupported(root, sheet, 'delete_rows');
|
|
334
|
+
for (let row = startRow; row < startRow + count; row += 1)
|
|
335
|
+
delete sheet.cellData[String(row)];
|
|
336
|
+
const rows = Object.keys(sheet.cellData).map(Number).filter(Number.isInteger).sort((a, b) => a - b);
|
|
337
|
+
for (const row of rows) {
|
|
338
|
+
if (row < startRow + count)
|
|
339
|
+
continue;
|
|
340
|
+
sheet.cellData[String(row - count)] = sheet.cellData[String(row)];
|
|
341
|
+
delete sheet.cellData[String(row)];
|
|
342
|
+
}
|
|
343
|
+
sheet.rowCount = Math.max(1, sheet.rowCount - count);
|
|
344
|
+
recomputeDataSheetStats(sheet);
|
|
345
|
+
}
|
|
346
|
+
function insertColumns(root, sheetRef, startColumn, count) {
|
|
347
|
+
validateIndex(startColumn, 'startColumn');
|
|
348
|
+
validateCount(count);
|
|
349
|
+
const sheet = resolveSheet(root, sheetRef);
|
|
350
|
+
if (sheet.columnCount + count > SHEET_MAX_COLUMNS)
|
|
351
|
+
throw new Error(`插入后列数超过上限(最多 ${SHEET_MAX_COLUMNS} 列)`);
|
|
352
|
+
assertStructuralOperationSupported(root, sheet, 'insert_columns');
|
|
353
|
+
if (maxCellIndex(sheet.cellData, 'column') + count >= SHEET_MAX_COLUMNS)
|
|
354
|
+
throw new Error('插入后单元格超出工作簿列上限');
|
|
355
|
+
for (const cells of Object.values(sheet.cellData)) {
|
|
356
|
+
const nextCells = {};
|
|
357
|
+
for (const [columnKey, cell] of Object.entries(cells)) {
|
|
358
|
+
const column = Number(columnKey);
|
|
359
|
+
if (!Number.isInteger(column))
|
|
360
|
+
continue;
|
|
361
|
+
nextCells[String(column >= startColumn ? column + count : column)] = cell;
|
|
362
|
+
}
|
|
363
|
+
Object.keys(cells).forEach(column => { delete cells[column]; });
|
|
364
|
+
Object.assign(cells, nextCells);
|
|
365
|
+
}
|
|
366
|
+
sheet.columnCount += count;
|
|
367
|
+
recomputeDataSheetStats(sheet);
|
|
368
|
+
}
|
|
369
|
+
function deleteColumns(root, sheetRef, startColumn, count) {
|
|
370
|
+
validateIndex(startColumn, 'startColumn');
|
|
371
|
+
validateCount(count);
|
|
372
|
+
if (startColumn >= SHEET_MAX_COLUMNS || startColumn + count > SHEET_MAX_COLUMNS) {
|
|
373
|
+
throw new Error(`删除范围超过上限(最多 ${SHEET_MAX_COLUMNS} 列)`);
|
|
374
|
+
}
|
|
375
|
+
const endColumn = startColumn + count;
|
|
376
|
+
const sheet = resolveSheet(root, sheetRef);
|
|
377
|
+
assertStructuralOperationSupported(root, sheet, 'delete_columns');
|
|
378
|
+
for (const cells of Object.values(sheet.cellData)) {
|
|
379
|
+
const nextCells = {};
|
|
380
|
+
for (const [columnKey, cell] of Object.entries(cells)) {
|
|
381
|
+
const column = Number(columnKey);
|
|
382
|
+
if (!Number.isInteger(column) || (column >= startColumn && column < endColumn))
|
|
383
|
+
continue;
|
|
384
|
+
nextCells[String(column >= endColumn ? column - count : column)] = cell;
|
|
385
|
+
}
|
|
386
|
+
Object.keys(cells).forEach(column => { delete cells[column]; });
|
|
387
|
+
Object.assign(cells, nextCells);
|
|
388
|
+
}
|
|
389
|
+
sheet.columnCount = Math.max(1, sheet.columnCount - count);
|
|
390
|
+
recomputeDataSheetStats(sheet);
|
|
391
|
+
}
|
|
392
|
+
function createSheet(root, sheetId, name) {
|
|
393
|
+
const sheetName = name?.trim();
|
|
394
|
+
if (sheetName === undefined || sheetName.length === 0)
|
|
395
|
+
throw new Error('sheet 名称不能为空');
|
|
396
|
+
if (Object.values(root.sheets).some(sheet => sheet.name === sheetName))
|
|
397
|
+
throw new Error(`sheet 已存在:${sheetName}`);
|
|
398
|
+
const id = sheetId?.trim() || `sheet${Date.now().toString(36)}`;
|
|
399
|
+
if (root.sheets[id] !== undefined)
|
|
400
|
+
throw new Error(`sheet id 已存在:${id}`);
|
|
401
|
+
root.sheets[id] = { id, name: sheetName, rowCount: DEFAULT_SHEET_ROWS, columnCount: DEFAULT_SHEET_COLUMNS, cellData: {} };
|
|
402
|
+
root.sheetOrder.push(id);
|
|
403
|
+
}
|
|
404
|
+
function deleteSheet(root, sheetRef) {
|
|
405
|
+
if (root.sheetOrder.length <= 1)
|
|
406
|
+
throw new Error('工作簿至少需要保留一个 sheet,不能删除最后一个 sheet');
|
|
407
|
+
const sheet = resolveSheet(root, sheetRef);
|
|
408
|
+
delete root.sheets[sheet.id];
|
|
409
|
+
root.sheetOrder = root.sheetOrder.filter(id => id !== sheet.id);
|
|
410
|
+
}
|
|
411
|
+
function renameSheet(root, sheetRef, name) {
|
|
412
|
+
const sheetName = name?.trim();
|
|
413
|
+
if (sheetName === undefined || sheetName.length === 0)
|
|
414
|
+
throw new Error('sheet 名称不能为空');
|
|
415
|
+
const sheet = resolveSheet(root, sheetRef);
|
|
416
|
+
if (Object.values(root.sheets).some(candidate => candidate.name === sheetName && candidate.id !== sheet.id))
|
|
417
|
+
throw new Error(`sheet 已存在:${sheetName}`);
|
|
418
|
+
sheet.name = sheetName;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Mirrored by the browser projection's `applyAppendRows`, which runs the same
|
|
422
|
+
* check against live Univer state. The hint names `headerRowIndex` because that
|
|
423
|
+
* is what unblocks a report sheet whose real header is not row 0.
|
|
424
|
+
*/
|
|
425
|
+
export function formatCells(root, sheetRef, range, style) {
|
|
426
|
+
validateRange(range);
|
|
427
|
+
if (style === undefined || typeof style !== 'object')
|
|
428
|
+
throw new Error('format_cells 必须提供 style');
|
|
429
|
+
const sheet = resolveSheet(root, sheetRef);
|
|
430
|
+
for (let row = range.startRow; row <= range.endRow; row += 1) {
|
|
431
|
+
const cells = (sheet.cellData[String(row)] ??= {});
|
|
432
|
+
for (let column = range.startColumn; column <= range.endColumn; column += 1) {
|
|
433
|
+
const current = cells[String(column)] ?? { v: null };
|
|
434
|
+
cells[String(column)] = { ...current, s: { ...(current.s ?? {}), ...style } };
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function mergeCells(root, sheetRef, range) {
|
|
439
|
+
validateRange(range);
|
|
440
|
+
const sheet = resolveSheet(root, sheetRef);
|
|
441
|
+
const merges = Array.isArray(sheet.mergeData) ? sheet.mergeData : [];
|
|
442
|
+
if (!merges.some(item => JSON.stringify(item) === JSON.stringify(range)))
|
|
443
|
+
merges.push(range);
|
|
444
|
+
sheet.mergeData = merges;
|
|
445
|
+
}
|
|
446
|
+
function unmergeCells(root, sheetRef, range) {
|
|
447
|
+
validateRange(range);
|
|
448
|
+
const sheet = resolveSheet(root, sheetRef);
|
|
449
|
+
const merges = Array.isArray(sheet.mergeData) ? sheet.mergeData : [];
|
|
450
|
+
sheet.mergeData = merges.filter(item => !rangesIntersect(item, range));
|
|
451
|
+
}
|
|
452
|
+
function rangesIntersect(left, right) {
|
|
453
|
+
return left.startRow <= right.endRow && right.startRow <= left.endRow
|
|
454
|
+
&& left.startColumn <= right.endColumn && right.startColumn <= left.endColumn;
|
|
455
|
+
}
|
|
456
|
+
function freezePanes(root, sheetRef, freeze) {
|
|
457
|
+
const sheet = resolveSheet(root, sheetRef);
|
|
458
|
+
if (freeze === undefined || (freeze.rows ?? 0) < 0 || (freeze.columns ?? 0) < 0)
|
|
459
|
+
throw new Error('freeze_panes 参数无效');
|
|
460
|
+
sheet.freeze = { rows: Math.floor(freeze.rows ?? 0), columns: Math.floor(freeze.columns ?? 0) };
|
|
461
|
+
}
|
|
462
|
+
function setDimension(root, sheetRef, key, index, size) {
|
|
463
|
+
validateIndex(index, `${key} index`);
|
|
464
|
+
if (key === 'columnData' && index >= SHEET_MAX_COLUMNS)
|
|
465
|
+
throw new Error(`columnData index 超过上限(最多 ${SHEET_MAX_COLUMNS} 列)`);
|
|
466
|
+
if (key === 'rowData' && index >= SHEET_MAX_PHYSICAL_ROWS)
|
|
467
|
+
throw new Error(`rowData index 超过上限(最多 ${SHEET_MAX_ROWS} 条数据行 + 表头)`);
|
|
468
|
+
if (typeof size !== 'number' || !Number.isFinite(size) || size <= 0 || size > 1000)
|
|
469
|
+
throw new Error('尺寸必须是 0 到 1000 之间的有限数字');
|
|
470
|
+
const sheet = resolveSheet(root, sheetRef);
|
|
471
|
+
const data = sheet[key] ?? {};
|
|
472
|
+
const current = typeof data[String(index)] === 'object' && data[String(index)] !== null ? data[String(index)] : {};
|
|
473
|
+
const dimension = key === 'columnData' ? 'width' : 'height';
|
|
474
|
+
const otherDimension = key === 'columnData' ? 'height' : 'width';
|
|
475
|
+
const next = { ...current, [dimension]: size };
|
|
476
|
+
delete next[otherDimension];
|
|
477
|
+
data[String(index)] = next;
|
|
478
|
+
sheet[key] = data;
|
|
479
|
+
}
|
|
480
|
+
function sortRange(root, sheetRef, range, sort) {
|
|
481
|
+
validateRange(range);
|
|
482
|
+
if (sort === undefined || !Number.isInteger(sort.column) || sort.column < range.startColumn || sort.column > range.endColumn)
|
|
483
|
+
throw new Error('sort_range 的排序列必须位于 range 内');
|
|
484
|
+
const sheet = resolveSheet(root, sheetRef);
|
|
485
|
+
const rows = [];
|
|
486
|
+
for (let row = range.startRow; row <= range.endRow; row += 1) {
|
|
487
|
+
const selected = {};
|
|
488
|
+
const source = sheet.cellData[String(row)] ?? {};
|
|
489
|
+
for (let column = range.startColumn; column <= range.endColumn; column += 1) {
|
|
490
|
+
if (source[String(column)] !== undefined)
|
|
491
|
+
selected[String(column)] = source[String(column)];
|
|
492
|
+
}
|
|
493
|
+
rows.push({ index: row, cells: selected });
|
|
494
|
+
}
|
|
495
|
+
const valueOf = (item) => {
|
|
496
|
+
const value = toCellValue(item.cells[String(sort.column)]);
|
|
497
|
+
return typeof value === 'number' ? value : String(value ?? '');
|
|
498
|
+
};
|
|
499
|
+
rows.sort((left, right) => {
|
|
500
|
+
const a = valueOf(left);
|
|
501
|
+
const b = valueOf(right);
|
|
502
|
+
const comparison = typeof a === 'number' && typeof b === 'number' ? a - b : String(a).localeCompare(String(b), 'zh-CN');
|
|
503
|
+
return sort.direction === 'desc' ? -comparison : comparison;
|
|
504
|
+
});
|
|
505
|
+
rows.forEach((item, offset) => {
|
|
506
|
+
const target = range.startRow + offset;
|
|
507
|
+
const current = sheet.cellData[String(target)] ?? {};
|
|
508
|
+
for (let column = range.startColumn; column <= range.endColumn; column += 1)
|
|
509
|
+
delete current[String(column)];
|
|
510
|
+
Object.assign(current, item.cells);
|
|
511
|
+
if (Object.keys(current).length > 0)
|
|
512
|
+
sheet.cellData[String(target)] = current;
|
|
513
|
+
else
|
|
514
|
+
delete sheet.cellData[String(target)];
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
function setHyperlink(root, sheetRef, range, url, label) {
|
|
518
|
+
validateRange(range);
|
|
519
|
+
const targetUrl = url?.trim();
|
|
520
|
+
if (targetUrl === undefined || targetUrl.length === 0)
|
|
521
|
+
throw new Error('set_hyperlink.url 不能为空');
|
|
522
|
+
if (!isSafeHyperlink(targetUrl))
|
|
523
|
+
throw new Error('set_hyperlink.url 只支持 http(s)、mailto、file 或 sheet 内部链接');
|
|
524
|
+
const sheet = resolveSheet(root, sheetRef);
|
|
525
|
+
const row = (sheet.cellData[String(range.startRow)] ??= {});
|
|
526
|
+
const current = row[String(range.startColumn)] ?? {};
|
|
527
|
+
const display = label === undefined || label.trim().length === 0 ? String(toCellValue(current) ?? targetUrl) : label;
|
|
528
|
+
const document = isRecord(current.p) ? current.p : {};
|
|
529
|
+
const oldBody = isRecord(document.body) ? document.body : {};
|
|
530
|
+
const oldRanges = Array.isArray(oldBody.customRanges) ? oldBody.customRanges.filter(item => isRecord(item) && item.rangeType !== 0) : [];
|
|
531
|
+
const linkId = `dsh-link-${randomUUID()}`;
|
|
532
|
+
row[String(range.startColumn)] = {
|
|
533
|
+
...current,
|
|
534
|
+
v: display,
|
|
535
|
+
t: 1,
|
|
536
|
+
p: {
|
|
537
|
+
...document,
|
|
538
|
+
id: typeof document.id === 'string' ? document.id : `dsh-doc-${randomUUID()}`,
|
|
539
|
+
documentStyle: isRecord(document.documentStyle) ? document.documentStyle : {},
|
|
540
|
+
body: {
|
|
541
|
+
...oldBody,
|
|
542
|
+
dataStream: `${display}\r\n`,
|
|
543
|
+
customRanges: [
|
|
544
|
+
...oldRanges,
|
|
545
|
+
{ startIndex: 0, endIndex: Math.max(0, display.length - 1), rangeId: linkId, rangeType: 0, properties: { url: targetUrl } },
|
|
546
|
+
],
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
sheet.rowCount = Math.max(sheet.rowCount, range.startRow + 1);
|
|
551
|
+
sheet.columnCount = Math.max(sheet.columnCount, range.startColumn + 1);
|
|
552
|
+
recomputeDataSheetStats(sheet);
|
|
553
|
+
}
|
|
554
|
+
function removeHyperlink(root, sheetRef, range) {
|
|
555
|
+
validateRange(range);
|
|
556
|
+
const sheet = resolveSheet(root, sheetRef);
|
|
557
|
+
const row = sheet.cellData[String(range.startRow)];
|
|
558
|
+
const current = row?.[String(range.startColumn)];
|
|
559
|
+
if (!row || !isRecord(current) || !isRecord(current.p) || !isRecord(current.p.body))
|
|
560
|
+
return;
|
|
561
|
+
const body = current.p.body;
|
|
562
|
+
if (!Array.isArray(body.customRanges))
|
|
563
|
+
return;
|
|
564
|
+
const customRanges = body.customRanges.filter(item => !(isRecord(item) && item.rangeType === 0));
|
|
565
|
+
if (customRanges.length === body.customRanges.length)
|
|
566
|
+
return;
|
|
567
|
+
current.p = { ...current.p, body: { ...body, customRanges } };
|
|
568
|
+
recomputeDataSheetStats(sheet);
|
|
569
|
+
}
|
|
570
|
+
function withResolvedDuplicateRows(snapshot, payload) {
|
|
571
|
+
validateRange(payload.range);
|
|
572
|
+
const sheet = resolveSheet(asWorkbookSnapshot(snapshot), payload.sheetId ?? payload.sheetName);
|
|
573
|
+
const range = payload.range;
|
|
574
|
+
const values = rangeValues(sheet, range);
|
|
575
|
+
const removedRows = findDuplicateRows(values, range, payload.dedupeColumns, payload.hasHeader ?? false, payload.keep ?? 'first');
|
|
576
|
+
return { ...payload, removedRows };
|
|
577
|
+
}
|
|
578
|
+
/** Resolve data-dependent children before an operation is persisted/replayed. */
|
|
579
|
+
export function resolveOperationPayload(snapshot, type, payload) {
|
|
580
|
+
if (type !== 'batch') {
|
|
581
|
+
if (type === 'snapshot_replace')
|
|
582
|
+
validateSnapshotBounds(payload.snapshot);
|
|
583
|
+
return type === 'remove_duplicates' ? withResolvedDuplicateRows(snapshot, payload) : payload;
|
|
584
|
+
}
|
|
585
|
+
if (!Array.isArray(payload.operations) || payload.operations.length === 0 || payload.operations.length > 200) {
|
|
586
|
+
throw new Error('batch.operations 必须包含 1 到 200 个操作');
|
|
587
|
+
}
|
|
588
|
+
// Normal Agent/browser batches contain already validated, replayable
|
|
589
|
+
// payloads. Only the legacy data-dependent dedupe operation needs a
|
|
590
|
+
// sequential resolution pass; skipping the clone here is significant for
|
|
591
|
+
// large append/update batches.
|
|
592
|
+
if (!payload.operations.some(operation => operationNeedsPayloadResolution(operation)))
|
|
593
|
+
return payload;
|
|
594
|
+
let workingSnapshot = cloneSnapshot(snapshot);
|
|
595
|
+
const operations = (payload.operations ?? []).map((child, index) => {
|
|
596
|
+
const childPayload = resolveOperationPayload(workingSnapshot, child.type, child.payload);
|
|
597
|
+
const childOperation = {
|
|
598
|
+
opId: `resolve:${index}`,
|
|
599
|
+
workbookId: 'resolve',
|
|
600
|
+
revision: 0,
|
|
601
|
+
baseRevision: 0,
|
|
602
|
+
actor: 'agent',
|
|
603
|
+
timestamp: new Date(0).toISOString(),
|
|
604
|
+
type: child.type,
|
|
605
|
+
payload: childPayload,
|
|
606
|
+
};
|
|
607
|
+
workingSnapshot = applyWorkbookOperation(workingSnapshot, childOperation);
|
|
608
|
+
return { ...child, payload: childPayload };
|
|
609
|
+
});
|
|
610
|
+
return { ...payload, operations };
|
|
611
|
+
}
|
|
612
|
+
function operationNeedsPayloadResolution(operation) {
|
|
613
|
+
return operation.type === 'remove_duplicates'
|
|
614
|
+
|| operation.type === 'batch' && (operation.payload.operations ?? []).some(operationNeedsPayloadResolution);
|
|
615
|
+
}
|
|
616
|
+
function removeDuplicateRows(root, sheetRef, payload) {
|
|
617
|
+
validateRange(payload.range);
|
|
618
|
+
const sheet = resolveSheet(root, sheetRef);
|
|
619
|
+
const rows = payload.removedRows === undefined
|
|
620
|
+
? findDuplicateRows(rangeValues(sheet, payload.range), payload.range, payload.dedupeColumns, payload.hasHeader ?? false, payload.keep ?? 'first')
|
|
621
|
+
: payload.removedRows;
|
|
622
|
+
if (rows.length === 0)
|
|
623
|
+
return;
|
|
624
|
+
for (const row of rows)
|
|
625
|
+
validateIndex(row, 'removedRows');
|
|
626
|
+
// Delete from the bottom so indexes remain the original range indexes.
|
|
627
|
+
let end = rows[rows.length - 1];
|
|
628
|
+
let start = end;
|
|
629
|
+
for (let index = rows.length - 2; index >= -1; index -= 1) {
|
|
630
|
+
const current = index >= 0 ? rows[index] : undefined;
|
|
631
|
+
if (current !== undefined && current === start - 1) {
|
|
632
|
+
start = current;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
deleteRows(root, sheetRef, start, end - start + 1);
|
|
636
|
+
if (current !== undefined) {
|
|
637
|
+
end = current;
|
|
638
|
+
start = current;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
recomputeDataSheetStats(sheet);
|
|
642
|
+
}
|
|
643
|
+
function rangeValues(sheet, range) {
|
|
644
|
+
const values = [];
|
|
645
|
+
for (let row = range.startRow; row <= range.endRow; row += 1) {
|
|
646
|
+
const source = sheet.cellData[String(row)] ?? {};
|
|
647
|
+
values.push(Array.from({ length: range.endColumn - range.startColumn + 1 }, (_value, offset) => toCellValue(source[String(range.startColumn + offset)])));
|
|
648
|
+
}
|
|
649
|
+
return values;
|
|
650
|
+
}
|
|
651
|
+
function isSafeHyperlink(url) {
|
|
652
|
+
return /^(?:https?:|mailto:|file:|#)/i.test(url);
|
|
653
|
+
}
|
|
654
|
+
function isRecord(value) {
|
|
655
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
656
|
+
}
|
|
657
|
+
function validateCount(count) {
|
|
658
|
+
if (!Number.isInteger(count) || count < 1 || count > SHEET_MAX_ROWS)
|
|
659
|
+
throw new Error('count 必须是 1 到上限之间的整数');
|
|
660
|
+
}
|
|
661
|
+
function coerceCellValue(type, value) {
|
|
662
|
+
if (type === 'number' || type === 'currency') {
|
|
663
|
+
const numeric = typeof value === 'number' ? value : Number(value);
|
|
664
|
+
return Number.isFinite(numeric) ? numeric : String(value);
|
|
665
|
+
}
|
|
666
|
+
if (type === 'boolean')
|
|
667
|
+
return typeof value === 'boolean' ? value : String(value) === 'true';
|
|
668
|
+
if (type === 'date')
|
|
669
|
+
return value instanceof Date ? value.toISOString().slice(0, 10) : String(value);
|
|
670
|
+
return String(value);
|
|
671
|
+
}
|
|
672
|
+
function sameSheetColumns(left, right) {
|
|
673
|
+
return left.length === right.length && left.every((column, index) => {
|
|
674
|
+
const other = right[index];
|
|
675
|
+
return other !== undefined && column.key === other.key && column.label === other.label && column.type === other.type;
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
function assertNeverOperation(value) {
|
|
679
|
+
throw new Error(`不支持的 workbook operation:${String(value)}`);
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Appends schema-typed rows below the header row.
|
|
683
|
+
*
|
|
684
|
+
* `headerRowIndex` and `verifyHeader` exist for report sheets whose real header
|
|
685
|
+
* is not row 0 — previously those sheets could only be written through
|
|
686
|
+
* `update_cells`, which is what produced the "use update_cells" dead end. Both
|
|
687
|
+
* default to the old behaviour (header on row 0, labels verified), and the
|
|
688
|
+
* default path below is left structurally identical so no stored workbook or
|
|
689
|
+
* existing caller changes meaning.
|
|
690
|
+
*/
|
|
691
|
+
function appendRows(root, sheetRef, columns, rows, meta, sheetName, rawHeaderRowIndex, rawVerifyHeader) {
|
|
692
|
+
if (columns.length === 0 || columns.length > SHEET_MAX_COLUMNS)
|
|
693
|
+
throw new Error(`列数必须是 1 到 ${SHEET_MAX_COLUMNS}`);
|
|
694
|
+
if (rows.length > SHEET_MAX_ROWS)
|
|
695
|
+
throw new Error(`一次追加最多 ${SHEET_MAX_ROWS} 行`);
|
|
696
|
+
const headerRowIndex = rawHeaderRowIndex ?? 0;
|
|
697
|
+
if (!Number.isInteger(headerRowIndex) || headerRowIndex < 0)
|
|
698
|
+
throw new Error('headerRowIndex 必须是大于等于 0 的整数');
|
|
699
|
+
if (headerRowIndex >= SHEET_MAX_PHYSICAL_ROWS)
|
|
700
|
+
throw new Error('headerRowIndex 超出上限(最后一个物理行是 ' + SHEET_MAX_ROWS + ')');
|
|
701
|
+
// Only an explicit false relaxes the label comparison; it remains the guard
|
|
702
|
+
// that catches a column reorder before rows are written.
|
|
703
|
+
const verifyHeader = rawVerifyHeader !== false;
|
|
704
|
+
let sheet;
|
|
705
|
+
try {
|
|
706
|
+
sheet = resolveSheet(root, sheetRef);
|
|
707
|
+
}
|
|
708
|
+
catch (error) {
|
|
709
|
+
if (sheetRef === undefined)
|
|
710
|
+
throw error;
|
|
711
|
+
// Use the caller's stable reference as the id when append_rows creates a
|
|
712
|
+
// sheet. This keeps the Node snapshot, browser projection, and subsequent
|
|
713
|
+
// commands in the same batch on the exact same sheet.
|
|
714
|
+
createSheet(root, sheetRef, sheetName ?? sheetRef);
|
|
715
|
+
sheet = resolveSheet(root, sheetRef);
|
|
716
|
+
}
|
|
717
|
+
const headerKey = String(headerRowIndex);
|
|
718
|
+
const header = sheet.cellData[headerKey];
|
|
719
|
+
const existingSchema = isSheetSchema(sheet.dataSheetSchema) ? sheet.dataSheetSchema : undefined;
|
|
720
|
+
if (verifyHeader && existingSchema !== undefined && !sameSheetColumns(existingSchema.columns, columns)) {
|
|
721
|
+
throw new Error(APPEND_HEADER_MISMATCH);
|
|
722
|
+
}
|
|
723
|
+
const existingHeaderIndexes = Object.keys(header ?? {}).map(Number).filter(Number.isInteger);
|
|
724
|
+
const existingColumnCount = existingHeaderIndexes.length === 0 ? 0 : Math.max(...existingHeaderIndexes) + 1;
|
|
725
|
+
const existingHeader = header === undefined ? [] : Array.from({ length: existingColumnCount }, (_value, index) => header[String(index)]?.v);
|
|
726
|
+
if (verifyHeader && existingColumnCount > 0 && (existingColumnCount !== columns.length
|
|
727
|
+
|| columns.some((column, index) => String(existingHeader[index] ?? '') !== column.label))) {
|
|
728
|
+
throw new Error(APPEND_HEADER_MISMATCH);
|
|
729
|
+
}
|
|
730
|
+
if (header === undefined || Object.keys(header).length === 0) {
|
|
731
|
+
sheet.cellData[headerKey] = Object.fromEntries(columns.map((column, index) => [String(index), { v: column.label }]));
|
|
732
|
+
}
|
|
733
|
+
const storedStats = sheet.dataSheetStats;
|
|
734
|
+
const storedDataRows = typeof storedStats?.rowCount === 'number' && Number.isInteger(storedStats.rowCount) && storedStats.rowCount >= 0
|
|
735
|
+
? storedStats.rowCount
|
|
736
|
+
: -1;
|
|
737
|
+
const floorRow = headerRowIndex + 1;
|
|
738
|
+
let existingDataRows = -1;
|
|
739
|
+
let nextRow = floorRow;
|
|
740
|
+
if (headerRowIndex === 0 && storedDataRows >= 0) {
|
|
741
|
+
existingDataRows = storedDataRows;
|
|
742
|
+
nextRow = Math.max(floorRow, existingDataRows + 1);
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
// A stored counter can only be read against a row-0 header, so a report
|
|
746
|
+
// sheet is positioned from its real last occupied row instead.
|
|
747
|
+
for (const key of Object.keys(sheet.cellData)) {
|
|
748
|
+
const row = Number(key);
|
|
749
|
+
if (Number.isInteger(row))
|
|
750
|
+
nextRow = Math.max(nextRow, row + 1);
|
|
751
|
+
}
|
|
752
|
+
existingDataRows = Math.max(0, nextRow - 1);
|
|
753
|
+
}
|
|
754
|
+
if (nextRow - 1 + rows.length > SHEET_MAX_ROWS)
|
|
755
|
+
throw new Error(`写入后行数超过上限(最多 ${SHEET_MAX_ROWS} 行)`);
|
|
756
|
+
for (const [rowIndex, input] of rows.entries()) {
|
|
757
|
+
const line = {};
|
|
758
|
+
columns.forEach((column, columnIndex) => {
|
|
759
|
+
const value = input[column.key];
|
|
760
|
+
if (value === undefined || value === null)
|
|
761
|
+
return;
|
|
762
|
+
line[String(columnIndex)] = { v: coerceCellValue(column.type, value) };
|
|
763
|
+
});
|
|
764
|
+
sheet.cellData[String(nextRow + rowIndex)] = line;
|
|
765
|
+
}
|
|
766
|
+
sheet.columnCount = Math.max(sheet.columnCount, columns.length);
|
|
767
|
+
sheet.rowCount = Math.max(sheet.rowCount, nextRow + rows.length);
|
|
768
|
+
sheet.dataSheetStats = {
|
|
769
|
+
rowCount: Math.max(existingDataRows, nextRow - 1 + rows.length),
|
|
770
|
+
columnCount: Math.max(typeof sheet.dataSheetStats?.columnCount === 'number'
|
|
771
|
+
? sheet.dataSheetStats.columnCount
|
|
772
|
+
: 0, columns.length),
|
|
773
|
+
};
|
|
774
|
+
if (meta !== undefined)
|
|
775
|
+
sheet.dataSheetMeta = meta;
|
|
776
|
+
sheet.dataSheetSchema = {
|
|
777
|
+
version: existingSchema?.version ?? 1,
|
|
778
|
+
columns,
|
|
779
|
+
rowCount: Math.max(existingDataRows, nextRow - 1 + rows.length),
|
|
780
|
+
updatedAt: new Date().toISOString(),
|
|
781
|
+
};
|
|
782
|
+
}
|