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