@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,987 @@
1
+ /**
2
+ * `workbook_edit`: one plan, many semantic Versions (docs/issue/0022 P3b).
3
+ *
4
+ * The largest tool surface by far: it parses command lists and version groups,
5
+ * expands `duplicate_sheet` and `valueFromFile`, resolves a table's columns from
6
+ * the sheet it will land on, and reports a receipt per imported file. It is one
7
+ * file because those steps are one transaction — a plan either commits whole or
8
+ * is rejected whole (PRD 0002 §5).
9
+ */
10
+ import { cloneSnapshot } from '../contract/snapshots.ts'
11
+ import { createHash, randomUUID } from 'node:crypto'
12
+ import { statSync } from 'node:fs'
13
+ import { readFile } from 'node:fs/promises'
14
+ import { extname, join, resolve } from 'node:path'
15
+ import * as XLSX from 'xlsx'
16
+ import { cellAddress } from '#contracts'
17
+ import {
18
+ SHEET_MAX_APPEND_ROWS,
19
+ SHEET_MAX_COLUMNS,
20
+ SHEET_MAX_PHYSICAL_ROWS,
21
+ SHEET_MAX_ROWS,
22
+ type SheetColumn,
23
+ type SheetColumnType,
24
+ type WorkbookCellUpdate,
25
+ type WorkbookOperationPayload,
26
+ type WorkbookOperationType,
27
+ type WorkbookRange,
28
+ } from '../types.ts'
29
+ import { DataSheetError } from '../errors.ts'
30
+
31
+ import { applyWorkbookOperation } from '../contract/mutations.ts'
32
+ import { workbookOperationSteps } from '../contract/operations.ts'
33
+ import type { LineageContext } from '#contracts'
34
+ import { mutateWorkbookPlan, readWorkbook } from '../workbook-manager.ts'
35
+ import { isOperationType, normalizeValueMatrix, parseIndex, parseRange } from '../request-params.ts'
36
+ import {
37
+ asToolArguments,
38
+ commandIndex,
39
+ commandPayload,
40
+ commandSheet,
41
+ now,
42
+ optionalAuditText,
43
+ optionalIntent,
44
+ parseCellDeleteShift,
45
+ parseCount,
46
+ parseHeaderVerification,
47
+ parseNonEmptyString,
48
+ sheetRefForSnapshot,
49
+ type ToolArguments,
50
+ } from './arguments.ts'
51
+ import {
52
+ chunkRows,
53
+ dimensionCommandRange,
54
+ dimensionRange,
55
+ parseApiValues,
56
+ parseCells,
57
+ parseFiniteNumber,
58
+ parseFreeze,
59
+ parseHyperlink,
60
+ parseHyperlinkLabel,
61
+ parseSort,
62
+ parseValueRange,
63
+ resolveCellFileValues,
64
+ validateSheet,
65
+ } from './cells.ts'
66
+ import { operationReceipt, sheetOverviews, sheetReferenceList } from './render.ts'
67
+ import { snapshotSheet } from './workbook-read.ts'
68
+
69
+ /** Agent-facing receipt seed for one `import_file` step. It is stripped before
70
+ * the command is persisted, so the Version/Step payload keeps its existing
71
+ * shape and replay model. */
72
+ export interface ImportFileMeta {
73
+ sourcePath: string
74
+ range: WorkbookRange
75
+ rowsWritten: number
76
+ cellsWritten: number
77
+ sourceRowCount: number
78
+ truncated: boolean
79
+ verifyCapacity: boolean
80
+ firstRow: unknown[]
81
+ lastRow: unknown[]
82
+ valuesHash: string
83
+ }
84
+
85
+ /** One parsed command plus non-persisted Agent-facing metadata. */
86
+ export interface ParsedCommand {
87
+ type: WorkbookOperationType
88
+ payload: WorkbookOperationPayload
89
+ importFile?: ImportFileMeta
90
+ }
91
+
92
+ /**
93
+ * Copy a sheet's cell content and column widths into a new sheet of the target
94
+ * workbook (docs/issue/0011 I2 / T7).
95
+ *
96
+ * Expanded into the existing `create_sheet` / `update_cells` /
97
+ * `set_column_width` commands rather than becoming a new persisted operation:
98
+ * the Version/Step model is untouched and the browser projection needs no new
99
+ * branch. The source is read from storage, so it has to exist before the batch
100
+ * starts — a sheet created earlier in the same batch is not visible here.
101
+ *
102
+ * What is copied is the content that was tedious to retype (title block, header
103
+ * row, long SQL text) plus column widths. Styles, merges, freeze panes and data
104
+ * validation are deliberately not copied; they stay with the source sheet.
105
+ */
106
+ export async function expandDuplicateSheet(raw: ToolArguments, index: number, targetWorkbookId: string, warnings: string[] = []): Promise<ParsedCommand[]> {
107
+ const label = `commands[${index}]`
108
+ const sourceWorkbookId = raw.fromWorkbookId === undefined || raw.fromWorkbookId === null
109
+ ? targetWorkbookId
110
+ : parseNonEmptyString(raw.fromWorkbookId, `${label}.fromWorkbookId`)
111
+ const name = parseNonEmptyString(raw.name, `${label}.name`)
112
+ const explicitId = raw.sheetId === undefined || raw.sheetId === null
113
+ ? undefined
114
+ : parseNonEmptyString(raw.sheetId, `${label}.sheetId`)
115
+ const source = await readWorkbook(sourceWorkbookId)
116
+ if (source === null) throw new Error(`${label}.fromWorkbookId 工作簿不存在:${sourceWorkbookId}`)
117
+ const root = asToolArguments(source.snapshot, 'workbook snapshot')
118
+ const order = Array.isArray(root.sheetOrder) ? root.sheetOrder.map(String) : Object.keys(asToolArguments(root.sheets, 'workbook snapshot.sheets'))
119
+ const requested = raw.sheet === undefined || raw.sheet === null
120
+ ? order[0]
121
+ : parseNonEmptyString(raw.sheet, `${label}.sheet`)
122
+ if (requested === undefined) throw new Error(`${label} 源工作簿没有可复制的 sheet`)
123
+ const sheet = snapshotSheet(source.snapshot, requested)
124
+ const newSheetId = explicitId ?? stableCreateSheetId(targetWorkbookId, name, 0, index)
125
+
126
+ const commands: ParsedCommand[] = [{ type: 'create_sheet', payload: { sheetId: newSheetId, name } }]
127
+ const cells: WorkbookCellUpdate[] = []
128
+ const cellData = sheet.cellData
129
+ if (typeof cellData === 'object' && cellData !== null) {
130
+ for (const [rowKey, line] of Object.entries(cellData as Record<string, Record<string, { v?: unknown; f?: unknown }>>)) {
131
+ const row = Number(rowKey)
132
+ if (!Number.isInteger(row) || row < 0) continue
133
+ for (const [columnKey, cell] of Object.entries(line ?? {})) {
134
+ const column = Number(columnKey)
135
+ if (!Number.isInteger(column) || column < 0) continue
136
+ if (cell === null || typeof cell !== 'object') continue
137
+ const value = cell.v === undefined ? null : cell.v
138
+ const formula = typeof cell.f === 'string' ? cell.f : undefined
139
+ // An empty cell carries nothing to reproduce.
140
+ if (value === null && formula === undefined) continue
141
+ cells.push({ row, column, value: value as WorkbookCellUpdate['value'], ...(formula === undefined ? {} : { formula }) })
142
+ }
143
+ }
144
+ }
145
+ if (cells.length === 0) {
146
+ warnings.push(`${label} 源 sheet「${requested}」没有可复制的内容,只创建了空 sheet`)
147
+ } else {
148
+ commands.push({ type: 'update_cells', payload: { sheetId: newSheetId, cells } })
149
+ }
150
+ const columnData = sheet.columnData
151
+ if (typeof columnData === 'object' && columnData !== null) {
152
+ for (const [key, entry] of Object.entries(columnData as Record<string, { width?: unknown }>)) {
153
+ const column = Number(key)
154
+ const width = entry?.width
155
+ if (!Number.isInteger(column) || typeof width !== 'number' || !(width > 0) || width > 1000) continue
156
+ commands.push({
157
+ type: 'set_column_width',
158
+ payload: { sheetId: newSheetId, range: { startRow: 0, endRow: 0, startColumn: column, endColumn: column }, size: width },
159
+ })
160
+ }
161
+ }
162
+ return commands
163
+ }
164
+
165
+ export async function parseEditCommands(value: unknown, warnings: string[] = [], targetWorkbookId = 'workbook'): Promise<ParsedCommand[]> {
166
+ if (!Array.isArray(value) || value.length === 0 || value.length > 100) throw new Error('commands 必须是 1 到 100 项的数组')
167
+ const aliases: Record<string, WorkbookOperationType> = {
168
+ set_values: 'update_cells',
169
+ clear: 'clear_range',
170
+ format: 'format_cells',
171
+ merge: 'merge_cells',
172
+ unmerge: 'unmerge_cells',
173
+ freeze: 'freeze_panes',
174
+ sort: 'sort_range',
175
+ }
176
+ const importedTables = await Promise.all(value.map(async (candidate, index) => {
177
+ const raw = commandPayload(asToolArguments(candidate, 'commands[' + index + ']'))
178
+ const rawType = raw.type
179
+ const type = typeof rawType === 'string' && aliases[rawType] !== undefined ? aliases[rawType] : rawType
180
+ if (raw.sourcePath === undefined) return undefined
181
+ // `update_cells` prefers a literal matrix or `cells` payload; reading a file
182
+ // the command will ignore would fail the whole plan when that path is
183
+ // missing and would waste I/O on a file that is never used.
184
+ const usesImport = type === 'import_file' || type === 'append_rows'
185
+ || (type === 'update_cells' && raw.cells === undefined && literalValueMatrix(raw) === undefined)
186
+ return usesImport ? readTabularSource(raw.sourcePath, raw.sourceFormat) : undefined
187
+ }))
188
+ // `duplicate_sheet` has to read its source workbook from storage, so it is
189
+ // the one command whose expansion cannot happen inside the synchronous loop
190
+ // below (which uses `return` to end each case). Resolve those expansions up
191
+ // front and inject them in place, so command order — and therefore the
192
+ // name-to-id binding in `bindEditVersionGroups` — is unchanged.
193
+ const duplicateExpansions = new Map<number, ParsedCommand[]>()
194
+ const fileCellExpansions = new Map<number, WorkbookCellUpdate[]>()
195
+ await Promise.all(value.map(async (candidate, index) => {
196
+ const raw = commandPayload(asToolArguments(candidate, 'commands[' + index + ']'))
197
+ if (raw.type === 'duplicate_sheet') {
198
+ duplicateExpansions.set(index, await expandDuplicateSheet(raw, index, targetWorkbookId, warnings))
199
+ return
200
+ }
201
+ // `cells[].valueFromFile` needs storage too, and `parseCells` is synchronous.
202
+ if (raw.type === 'update_cells' && Array.isArray(raw.cells)) {
203
+ const needsFile = raw.cells.some(cell => (
204
+ typeof cell === 'object' && cell !== null && !Array.isArray(cell)
205
+ && (cell as Record<string, unknown>).valueFromFile !== undefined
206
+ && (cell as Record<string, unknown>).valueFromFile !== null
207
+ ))
208
+ if (needsFile) {
209
+ fileCellExpansions.set(index, parseCells(await resolveCellFileValues(raw.cells, `commands[${index}]`)))
210
+ }
211
+ }
212
+ }))
213
+ const result: ParsedCommand[] = []
214
+ value.forEach((candidate, index) => {
215
+ const raw = commandPayload(asToolArguments(candidate, 'commands[' + index + ']'))
216
+ if (raw.scope !== undefined && raw.scope !== 'workbook' && raw.scope !== 'sheet' && raw.scope !== 'cell') {
217
+ throw new Error('commands[' + index + '].scope 必须是 workbook、sheet 或 cell')
218
+ }
219
+ const rawType = raw.type
220
+ const aliasedType = typeof rawType === 'string' && aliases[rawType] !== undefined ? aliases[rawType] : rawType
221
+ // `import_file` and `duplicate_sheet` are Agent-facing conveniences that are
222
+ // expanded below into the durable command set, so neither ever reaches the
223
+ // Version/Step model as a type of its own.
224
+ const type: WorkbookOperationType | 'import_file' | 'duplicate_sheet' =
225
+ aliasedType === 'import_file' || aliasedType === 'duplicate_sheet'
226
+ ? aliasedType
227
+ : aliasedType as WorkbookOperationType
228
+ if (type !== 'import_file' && type !== 'duplicate_sheet'
229
+ && (!isOperationType(type) || type === 'batch' || type === 'snapshot_replace' || type === 'remove_duplicates')) {
230
+ throw new Error('commands[' + index + '].type 不支持')
231
+ }
232
+ const sheet = commandSheet(raw, index, type === 'create_sheet' || type === 'append_rows')
233
+ switch (type) {
234
+ case 'update_cells': {
235
+ // A literal matrix wins. `sourcePath` is the bulk alternative, so a
236
+ // large result never has to be spelled out inside the tool call.
237
+ const imported = importedTables[index]
238
+ const matrix = literalValueMatrix(raw)
239
+ ?? (imported === undefined ? undefined : importedValueMatrix(imported, `commands[${index}]`))
240
+ const cells = fileCellExpansions.get(index) ?? (raw.cells === undefined
241
+ ? (() => {
242
+ const valueMatrix = normalizeValueMatrix(matrix)
243
+ return parseApiValues(valueMatrix, parseValueRange(valueCommandRange(raw), valueMatrix, `commands[${index}]`, warnings))
244
+ })()
245
+ : parseCells(raw.cells))
246
+ result.push({ type, payload: { sheetId: commandSheet(raw, index), cells } })
247
+ return
248
+ }
249
+ case 'clear_range':
250
+ result.push({ type, payload: { sheetId: commandSheet(raw, index), range: parseRange(raw.range) } })
251
+ return
252
+ case 'delete_cells':
253
+ result.push({ type, payload: { sheetId: commandSheet(raw, index), range: parseRange(raw.range), shift: parseCellDeleteShift(raw.shift) } })
254
+ return
255
+ case 'insert_rows':
256
+ case 'delete_rows': {
257
+ const range = dimensionCommandRange(raw, index, 'row')
258
+ result.push({ type, payload: { sheetId: commandSheet(raw, index), range, count: raw.count === undefined ? range.endRow - range.startRow + 1 : parseCount(raw.count) } })
259
+ return
260
+ }
261
+ case 'insert_columns':
262
+ case 'delete_columns': {
263
+ const range = dimensionCommandRange(raw, index, 'column')
264
+ result.push({ type, payload: { sheetId: commandSheet(raw, index), range, count: raw.count === undefined ? range.endColumn - range.startColumn + 1 : parseCount(raw.count) } })
265
+ return
266
+ }
267
+ case 'create_sheet':
268
+ result.push({ type, payload: { sheetId: sheet, name: parseNonEmptyString(raw.name, 'commands[' + index + '].name') } })
269
+ return
270
+ case 'delete_sheet':
271
+ result.push({ type, payload: { sheetId: commandSheet(raw, index) } })
272
+ return
273
+ case 'rename_sheet':
274
+ result.push({ type, payload: { sheetId: commandSheet(raw, index), name: parseNonEmptyString(raw.name, 'commands[' + index + '].name') } })
275
+ return
276
+ case 'append_rows': {
277
+ const label = `commands[${index}]`
278
+ const imported = importedTables[index]
279
+ const table = validateSheet({
280
+ name: raw.name ?? sheet ?? imported?.name ?? 'Sheet',
281
+ columns: raw.columns ?? imported?.columns,
282
+ rows: raw.rows ?? raw.data ?? raw.records ?? imported?.rows,
283
+ })
284
+ // Report sheets keep a title block above the real header row, so the
285
+ // header is addressable instead of being pinned to row 0.
286
+ const headerRowIndex = parseIndex(raw.headerRowIndex ?? 0, `${label}.headerRowIndex`)
287
+ if (headerRowIndex >= SHEET_MAX_PHYSICAL_ROWS) {
288
+ throw new Error(`${label}.headerRowIndex=${headerRowIndex} 超出上限(最后一个物理行是 ${SHEET_MAX_ROWS})`)
289
+ }
290
+ const verifyHeader = parseHeaderVerification(raw.verifyHeader, `${label}.verifyHeader`)
291
+ const chunks = table.rows.length === 0 ? [[]] : chunkRows(table.rows, SHEET_MAX_APPEND_ROWS)
292
+ for (const rows of chunks) {
293
+ result.push({ type, payload: { sheetId: sheet, name: table.name, columns: table.columns, rows, meta: parseSheetMeta(raw.meta), headerRowIndex, verifyHeader } })
294
+ }
295
+ return
296
+ }
297
+ case 'import_file': {
298
+ // A first-class "one command + one file path" bulk import for report
299
+ // sheets: the origin is explicit, row 0 never has to be a header, and
300
+ // the result carries a self-verifying receipt. It is normalized into
301
+ // the existing update_cells write path before persistence.
302
+ const label = `commands[${index}]`
303
+ const imported = importedTables[index]
304
+ if (imported === undefined) throw new Error(`${label}.sourcePath 是 import_file 的必填参数(支持 CSV/JSON/JSONL/XLSX/XLS)`)
305
+ const origin = raw.range === undefined || raw.range === null ? undefined : asToolArguments(raw.range, `${label}.range`)
306
+ const startValue = raw.startRow ?? origin?.startRow
307
+ if (startValue === undefined) throw new Error(`${label}.startRow 是 import_file 的必填参数(0-based 写入起点);也可用 range.startRow 给出`)
308
+ const startRow = parseIndex(startValue, `${label}.startRow`)
309
+ const startColumn = parseIndex(raw.startColumn ?? origin?.startColumn ?? 0, `${label}.startColumn`)
310
+ const source = importedValueMatrix(imported, label)
311
+ const available = SHEET_MAX_PHYSICAL_ROWS - startRow
312
+ if (available < 1) {
313
+ throw new Error(`${label}.startRow=${startRow} 超出上限(最多 ${SHEET_MAX_ROWS} 条数据行 + 表头,最后一个物理行是 ${SHEET_MAX_ROWS})`)
314
+ }
315
+ // A partial bulk write is a silent data-loss trap, so it must be asked
316
+ // for explicitly instead of being reported as a successful import.
317
+ if (source.length > available && raw.allowTruncate !== true) {
318
+ throw new Error(`${label} 源文件 ${source.length} 行超出可写区间:从第 ${startRow + 1} 行起最多还能写 ${available} 行(上限 ${SHEET_MAX_ROWS} 条数据行 + 表头)。请减少源文件行数、下移起点,或显式传 allowTruncate: true 只导入前 ${available} 行`)
319
+ }
320
+ const rows = source.length > available ? source.slice(0, available) : source
321
+ if (rows.length === 0) throw new Error(`${label}.startRow=${startRow} 没有可写入的行`)
322
+ const truncated = rows.length < source.length
323
+ const range = parseRange({ startRow, startColumn, endRow: startRow + rows.length - 1, endColumn: startColumn + (rows[0]?.length ?? 1) - 1 })
324
+ const cells = parseApiValues(rows, range)
325
+ result.push({
326
+ type: 'update_cells',
327
+ payload: { sheetId: commandSheet(raw, index), cells },
328
+ importFile: {
329
+ sourcePath: imported.path,
330
+ range,
331
+ rowsWritten: rows.length,
332
+ cellsWritten: cells.length,
333
+ sourceRowCount: source.length,
334
+ truncated,
335
+ verifyCapacity: raw.verifyCapacity !== false,
336
+ firstRow: rows[0] ?? [],
337
+ lastRow: rows[rows.length - 1] ?? [],
338
+ valuesHash: hashValueMatrix(rows),
339
+ },
340
+ })
341
+ return
342
+ }
343
+ case 'format_cells':
344
+ result.push({ type, payload: { sheetId: commandSheet(raw, index), range: parseRange(raw.range), style: parseStyle(raw.style) } })
345
+ return
346
+ case 'merge_cells':
347
+ case 'unmerge_cells':
348
+ result.push({ type, payload: { sheetId: commandSheet(raw, index), range: parseRange(raw.range) } })
349
+ return
350
+ case 'freeze_panes':
351
+ result.push({ type, payload: { sheetId: commandSheet(raw, index), freeze: parseFreeze(raw.freeze) } })
352
+ return
353
+ case 'set_column_width':
354
+ result.push({ type, payload: { sheetId: commandSheet(raw, index), range: dimensionRange(commandIndex(raw, index, 'index'), 'column'), size: parseFiniteNumber(raw.size, 'commands[' + index + '].size') } })
355
+ return
356
+ case 'set_row_height':
357
+ result.push({ type, payload: { sheetId: commandSheet(raw, index), range: dimensionRange(commandIndex(raw, index, 'index'), 'row'), size: parseFiniteNumber(raw.size, 'commands[' + index + '].size') } })
358
+ return
359
+ case 'sort_range':
360
+ result.push({ type, payload: { sheetId: commandSheet(raw, index), range: parseRange(raw.range), sort: parseSort(raw.sort) } })
361
+ return
362
+ case 'set_hyperlink':
363
+ result.push({ type, payload: { sheetId: commandSheet(raw, index), range: parseRange(raw.range), url: parseHyperlink(raw.url), label: raw.label === undefined ? undefined : parseHyperlinkLabel(raw.label) } })
364
+ return
365
+ case 'remove_hyperlink':
366
+ result.push({ type, payload: { sheetId: commandSheet(raw, index), range: parseRange(raw.range) } })
367
+ return
368
+ case 'duplicate_sheet':
369
+ result.push(...(duplicateExpansions.get(index) ?? []))
370
+ return
371
+ default:
372
+ assertNeverEditCommand(type)
373
+ }
374
+ })
375
+ return result
376
+ }
377
+
378
+ export interface ImportedTable {
379
+ /** Resolved absolute path of the source file. */
380
+ path: string
381
+ name?: string
382
+ columns?: unknown
383
+ rows: unknown[]
384
+ }
385
+
386
+ /**
387
+ * Expand a tabular import (`{ columns?, rows }`) into the rectangular value
388
+ * matrix the `update_cells` write path consumes. CSV/XLSX/JSONL produce row
389
+ * objects, so the column order is inferred from the row keys — the same rule
390
+ * `append_rows` uses — while `validateSheet` keeps the caps and error wording
391
+ * for both shapes. Ragged array rows are padded by `normalizeValueMatrix`.
392
+ */
393
+ export function importedValueMatrix(imported: ImportedTable, label: string): unknown[][] {
394
+ const rows = imported.rows
395
+ if (rows.length === 0) throw new Error(`${label}.sourcePath 没有可导入的数据行`)
396
+ if (rows.every(row => Array.isArray(row))) return normalizeValueMatrix(rows as unknown[][])
397
+ if (imported.columns !== undefined && !Array.isArray(imported.columns)) throw new Error(`${label}.sourcePath columns 必须是数组`)
398
+ const declared = Array.isArray(imported.columns) ? imported.columns : undefined
399
+ if (declared !== undefined && declared.length > 0) {
400
+ const table = validateSheet({ name: undefined, columns: declared, rows })
401
+ return (table.rows as Array<Record<string, unknown>>).map(row => table.columns.map(column => row[column.key] ?? null))
402
+ }
403
+ const keys: string[] = []
404
+ rows.forEach((row, index) => {
405
+ if (typeof row !== 'object' || row === null || Array.isArray(row)) throw new Error(`${label}.sourcePath 第 ${index + 1} 行不是对象`)
406
+ for (const key of Object.keys(row)) if (!keys.includes(key)) keys.push(key)
407
+ })
408
+ if (keys.length === 0) throw new Error(`${label}.sourcePath 无法从数据行推断列,请改用 CSV/XLSX 或显式提供 values`)
409
+ if (keys.length > SHEET_MAX_COLUMNS) throw new Error(`${label}.sourcePath 列数超过上限(最多 ${SHEET_MAX_COLUMNS} 列)`)
410
+ // The validator's sanitized keys are only metadata here: values are projected
411
+ // positionally by the original key order, exactly like `inferAppendTable`.
412
+ const table = validateSheet({
413
+ name: undefined,
414
+ columns: keys.map((key, index) => ({ key: /^[A-Za-z0-9_]+$/.test(key) ? key : `column_${index + 1}`, label: key, type: 'string' as SheetColumnType })),
415
+ rows,
416
+ })
417
+ if (table.columns.length !== keys.length) throw new Error(`${label}.sourcePath 列推断失败`)
418
+ return rows.map(row => keys.map(key => (row as Record<string, unknown>)[key] ?? null))
419
+ }
420
+
421
+ /** Stable content digest so an import receipt can prove what was written. */
422
+ export function hashValueMatrix(matrix: readonly unknown[][]): string {
423
+ return createHash('sha256').update(JSON.stringify(matrix)).digest('hex')
424
+ }
425
+
426
+ /**
427
+ * The literal value matrix of an `update_cells` command, if it carries one.
428
+ * Shared by the import pre-read and the command branch so both agree on when a
429
+ * `sourcePath` is actually consulted.
430
+ */
431
+ export function literalValueMatrix(raw: ToolArguments): unknown {
432
+ return raw.values ?? raw.data ?? raw.matrix
433
+ ?? (Array.isArray(raw.rows) && raw.rows.some(item => Array.isArray(item)) ? raw.rows : undefined)
434
+ }
435
+
436
+ /**
437
+ * `update_cells` anchors at `range`, or at the top-level `startRow` /
438
+ * `startColumn` pair. A missing anchor keeps the historical default (row 1,
439
+ * column 0) — the P1-2 origin change is intentionally out of scope here.
440
+ */
441
+ export function valueCommandRange(raw: ToolArguments): unknown {
442
+ if (raw.range !== undefined && raw.range !== null) return raw.range
443
+ if (raw.startRow === undefined && raw.startColumn === undefined) return undefined
444
+ return { startRow: raw.startRow, startColumn: raw.startColumn }
445
+ }
446
+
447
+ /** Read a local tabular file for bulk Agent imports. The file is converted to
448
+ * the same append_rows/update_cells contract before it reaches the durable
449
+ * mutator, so a failed parse never creates a partial workbook revision. */
450
+ export async function readTabularSource(rawPath: unknown, rawFormat: unknown): Promise<ImportedTable> {
451
+ const sourcePath = resolve(parseNonEmptyString(rawPath, 'sourcePath'))
452
+ let info: { size: number }
453
+ try { info = statSync(sourcePath) } catch { throw new Error(`sourcePath 文件不存在:${sourcePath}`) }
454
+ if (!Number.isInteger(info.size) || info.size < 0 || info.size > 128 * 1024 * 1024) throw new Error('sourcePath 文件必须不超过 128 MiB')
455
+ const extension = typeof rawFormat === 'string' && rawFormat.trim() !== '' ? `.${rawFormat.trim().replace(/^\./, '').toLowerCase()}` : extname(sourcePath).toLowerCase()
456
+ if (!['.csv', '.json', '.jsonl', '.xlsx', '.xls'].includes(extension)) throw new Error('sourcePath 仅支持 CSV、JSON、JSONL、XLSX、XLS')
457
+ const bytes = await readFile(sourcePath)
458
+ if (extension === '.json' || extension === '.jsonl') {
459
+ const text = bytes.toString('utf8').replace(/^\uFEFF/, '')
460
+ const value = extension === '.jsonl'
461
+ ? text.split(/\r?\n/).map(line => line.trim()).filter(Boolean).map((line, index) => {
462
+ try { return JSON.parse(line) as unknown } catch { throw new Error(`sourcePath JSONL 第 ${index + 1} 行不是有效 JSON`) }
463
+ })
464
+ : (() => { try { return JSON.parse(text) as unknown } catch { throw new Error('sourcePath JSON 不是有效 JSON') } })()
465
+ if (Array.isArray(value)) return { path: sourcePath, rows: value }
466
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
467
+ const record = value as Record<string, unknown>
468
+ const rows = record.rows ?? record.data ?? record.records
469
+ if (Array.isArray(rows)) return { path: sourcePath, name: typeof record.name === 'string' ? record.name : undefined, columns: record.columns, rows }
470
+ }
471
+ throw new Error('sourcePath JSON 须是数组,或包含 rows/data/records 数组的对象')
472
+ }
473
+ // CSV is text: decode UTF-8 explicitly so quoted commas/newlines parse
474
+ // correctly and CJK values do not come back as latin1 mojibake.
475
+ const workbook = XLSX.read(extension === '.csv' ? bytes.toString('utf8').replace(/^\uFEFF/, '') : bytes, extension === '.csv' ? { type: 'string' } : { type: 'buffer' })
476
+ const firstName = workbook.SheetNames[0]
477
+ if (firstName === undefined) throw new Error('sourcePath 工作簿没有 sheet')
478
+ const first = workbook.Sheets[firstName]
479
+ if (first === undefined) throw new Error('sourcePath 第一个 sheet 不存在')
480
+ return { path: sourcePath, name: firstName, rows: XLSX.utils.sheet_to_json(first, { defval: null }) as unknown[] }
481
+ }
482
+
483
+ export interface ParsedEditVersion {
484
+ summary: string
485
+ intent?: string
486
+ commands: ParsedCommand[]
487
+ }
488
+
489
+ /**
490
+ * Convert the tool protocol into the aggregate's two durable levels:
491
+ * semantic Version groups, each containing replayable command Steps. The
492
+ * shorthand keeps older callers working while making their whole request one
493
+ * coherent Version instead of one Version per command.
494
+ */
495
+ export async function parseEditVersionGroups(rawVersions: unknown, rawCommands: unknown, defaultIntent?: string, warnings: string[] = [], targetWorkbookId = 'workbook'): Promise<ParsedEditVersion[]> {
496
+ if (rawVersions !== undefined && rawVersions !== null) {
497
+ if (rawCommands !== undefined && rawCommands !== null) throw new Error('versions 与 commands 只能提供其中之一')
498
+ if (!Array.isArray(rawVersions) || rawVersions.length === 0 || rawVersions.length > 20) throw new Error('versions 必须是 1 到 20 项的数组')
499
+ const groups = await Promise.all(rawVersions.map(async (candidate, index) => {
500
+ const raw = asToolArguments(candidate, `versions[${index}]`)
501
+ const summary = optionalIntent(raw.summary, `versions[${index}].summary`)
502
+ if (summary === undefined) throw new Error(`versions[${index}].summary 必须是 1 到 280 字符的字符串`)
503
+ return {
504
+ summary,
505
+ intent: optionalAuditText(raw.intent, `versions[${index}].intent`, 1000) ?? defaultIntent,
506
+ commands: await parseEditCommands(raw.commands, warnings, targetWorkbookId),
507
+ }
508
+ }))
509
+ const commandCount = groups.reduce((total, group) => total + group.commands.length, 0)
510
+ if (commandCount > 100) throw new Error('versions 中 commands 总数最多 100 条')
511
+ return groups
512
+ }
513
+ const commands = await parseEditCommands(rawCommands, warnings, targetWorkbookId)
514
+ return [{
515
+ summary: defaultIntent ?? `Agent 编辑(${commands.length} 个 step)`,
516
+ intent: defaultIntent,
517
+ commands,
518
+ }]
519
+ }
520
+
521
+ export function assertNeverEditCommand(value: never): never {
522
+ throw new Error('不支持的编辑命令:' + String(value))
523
+ }
524
+
525
+ /**
526
+ * Bind the forgiving Agent wire format to the durable workbook format. This
527
+ * runs after the workbook has been loaded, so a single-sheet workbook can
528
+ * safely omit `sheet`, sheet names can be converted to ids, and commands that
529
+ * create a sheet can be followed by commands targeting that new sheet.
530
+ */
531
+ export function bindEditVersionGroups(snapshot: unknown, groups: ParsedEditVersion[], workbookId = 'workbook'): ParsedEditVersion[] {
532
+ let working = cloneSnapshot(snapshot)
533
+ return groups.map((group, groupIndex) => {
534
+ const commands = group.commands.map((command, commandIndex) => {
535
+ const label = `versions[${groupIndex}].commands[${commandIndex}]`
536
+ const payload = { ...command.payload }
537
+ if (command.type === 'create_sheet') {
538
+ // A missing id used to fall back to `sheet-${opId.slice(0,12)}`, which
539
+ // differs between the bind probe and the real commit and stranded
540
+ // later commands in the same batch on a stale id.
541
+ const explicit = payload.sheetId ?? payload.sheetName
542
+ payload.sheetId = explicit === undefined || String(explicit).trim() === ''
543
+ ? stableCreateSheetId(workbookId, payload.name, groupIndex, commandIndex)
544
+ : parseNonEmptyString(explicit, `${label}.sheet`)
545
+ delete payload.sheetName
546
+ } else {
547
+ const explicitRef = payload.sheetId ?? payload.sheetName
548
+ const ref = resolveCommandSheetRef(working, explicitRef, `${label}.sheet`, command.type === 'append_rows')
549
+ if (ref !== undefined) {
550
+ payload.sheetId = ref
551
+ delete payload.sheetName
552
+ }
553
+ }
554
+ if (command.type === 'append_rows' && (payload.columns === undefined || payload.columns.length === 0)) {
555
+ const inferred = inferAppendTable(working, payload.sheetId ?? payload.sheetName, payload.rows ?? [], label)
556
+ payload.columns = inferred.columns
557
+ payload.rows = inferred.rows
558
+ }
559
+ const bound: ParsedCommand = {
560
+ type: command.type,
561
+ payload,
562
+ ...(command.importFile === undefined ? {} : { importFile: command.importFile }),
563
+ }
564
+ working = applyWorkbookOperation(working, {
565
+ opId: `bind:${groupIndex}:${commandIndex}`,
566
+ workbookId: 'bind',
567
+ revision: 0,
568
+ actor: 'agent',
569
+ timestamp: new Date(0).toISOString(),
570
+ type: bound.type,
571
+ payload: bound.payload,
572
+ })
573
+ return bound
574
+ })
575
+ return { ...group, commands }
576
+ })
577
+ }
578
+
579
+ /** Deterministic create_sheet id so preview and commit address the same sheet. */
580
+ export function stableCreateSheetId(workbookId: string, name: unknown, groupIndex: number, commandIndex: number): string {
581
+ const seed = `${workbookId}#${groupIndex}#${commandIndex}#${String(name ?? 'sheet')}`
582
+ return `sheet-${createHash('sha256').update(seed).digest('hex').slice(0, 12)}`
583
+ }
584
+
585
+ /**
586
+ * Warn when one edit plan writes the same cell more than once. The later
587
+ * command wins, which is easy to do by accident in a 15-command batch.
588
+ */
589
+ export function collectDuplicateCellWrites(versions: readonly ParsedEditVersion[], warnings: string[]): void {
590
+ const firstWrite = new Map<string, string>()
591
+ const reported = new Set<string>()
592
+ let suppressed = 0
593
+ versions.forEach((version, groupIndex) => {
594
+ version.commands.forEach((command, commandIndex) => {
595
+ if (command.type !== 'update_cells') return
596
+ const sheetId = String(command.payload.sheetId ?? command.payload.sheetName ?? '')
597
+ const label = `versions[${groupIndex}].commands[${commandIndex}]`
598
+ for (const cell of command.payload.cells ?? []) {
599
+ const key = `${sheetId}:${cell.row}:${cell.column}`
600
+ const previous = firstWrite.get(key)
601
+ if (previous === undefined) {
602
+ firstWrite.set(key, label)
603
+ continue
604
+ }
605
+ if (reported.has(key)) continue
606
+ reported.add(key)
607
+ if (reported.size > 20) { suppressed += 1; continue }
608
+ warnings.push(`cells ${cellAddress(cell.row, cell.column)} 被 ${previous} 与 ${label} 依次覆盖;后写入的命令生效`)
609
+ }
610
+ })
611
+ })
612
+ if (suppressed > 0) warnings.push(`另有 ${suppressed} 个 cell 被重复写入,已省略`)
613
+ }
614
+
615
+ export function resolveCommandSheetRef(snapshot: unknown, explicit: unknown, label: string, allowMissing: boolean): string | undefined {
616
+ if (explicit === undefined || explicit === null || explicit === '') return sheetRefForSnapshot(snapshot, {}, label)
617
+ const ref = parseNonEmptyString(explicit, label)
618
+ try {
619
+ const sheet = snapshotSheet(snapshot, ref)
620
+ return typeof sheet.id === 'string' && sheet.id.length > 0 ? sheet.id : ref
621
+ } catch (error) {
622
+ if (allowMissing) return ref
623
+ throw new Error(`${label} 指向不存在的 sheet:${ref}。当前工作簿 sheet:${sheetReferenceList(snapshot)}。若该 sheet 由本批次 create_sheet 新建,请把 create_sheet 放在引用它的命令之前(同一个 version 内按顺序执行),或先单独提交 create_sheet`)
624
+ }
625
+ }
626
+
627
+ export function inferAppendTable(snapshot: unknown, sheetRef: unknown, rawRows: unknown, label: string): { columns: SheetColumn[]; rows: Array<Record<string, unknown>> } {
628
+ if (!Array.isArray(rawRows)) throw new Error(`${label}.rows 必须是对象数组`)
629
+ const rows = rawRows as Array<Record<string, unknown>>
630
+ let existing: Record<string, unknown> | undefined
631
+ if (sheetRef !== undefined) {
632
+ try { existing = snapshotSheet(snapshot, String(sheetRef)) } catch { /* append_rows may create a new sheet */ }
633
+ }
634
+ const schema = existing?.dataSheetSchema
635
+ const schemaRecord = typeof schema === 'object' && schema !== null && !Array.isArray(schema)
636
+ ? schema as Record<string, unknown>
637
+ : undefined
638
+ if (Array.isArray(schemaRecord?.columns) && schemaRecord.columns.length > 0) {
639
+ const columns = schemaRecord.columns as SheetColumn[]
640
+ return { columns, rows: projectRowsToColumns(rows, columns) }
641
+ }
642
+ const keys: string[] = []
643
+ for (const row of rows) {
644
+ for (const key of Object.keys(row)) if (!keys.includes(key)) keys.push(key)
645
+ }
646
+ if (keys.length === 0) throw new Error(`${label} 缺少 columns,且无法从 rows 推断列`)
647
+ if (keys.length > SHEET_MAX_COLUMNS) throw new Error(`推断列数超过上限(最多 ${SHEET_MAX_COLUMNS} 列)`)
648
+ const header = existing?.cellData
649
+ const headerRow = header !== undefined && typeof header === 'object' && header !== null && !Array.isArray(header)
650
+ ? (header as Record<string, unknown>)['0']
651
+ : undefined
652
+ const headerValues = headerRow !== null && typeof headerRow === 'object' && headerRow !== undefined && !Array.isArray(headerRow)
653
+ ? headerRow as Record<string, unknown>
654
+ : {}
655
+ const columns = keys.map((sourceKey, index) => {
656
+ const key = /^[A-Za-z0-9_]+$/.test(sourceKey) ? sourceKey : `column_${index + 1}`
657
+ const labelValue = headerValues[String(index)] && typeof headerValues[String(index)] === 'object'
658
+ ? (headerValues[String(index)] as Record<string, unknown>).v
659
+ : undefined
660
+ return { key, label: typeof labelValue === 'string' && labelValue.length > 0 ? labelValue : sourceKey, type: inferredColumnType(rows, sourceKey) }
661
+ })
662
+ const normalizedRows = rows.map(row => Object.fromEntries(columns.map((column, index) => [column.key, row[keys[index]]])))
663
+ return { columns, rows: normalizedRows }
664
+ }
665
+
666
+ export function projectRowsToColumns(rows: Array<Record<string, unknown>>, columns: SheetColumn[]): Array<Record<string, unknown>> {
667
+ return rows.map(row => Object.fromEntries(columns.map(column => {
668
+ if (Object.hasOwn(row, column.key)) return [column.key, row[column.key]]
669
+ if (Object.hasOwn(row, column.label)) return [column.key, row[column.label]]
670
+ return [column.key, undefined]
671
+ })))
672
+ }
673
+
674
+ export function inferredColumnType(rows: Array<Record<string, unknown>>, key: string): SheetColumnType {
675
+ const values = rows.map(row => row[key]).filter(value => value !== undefined && value !== null)
676
+ if (values.length > 0 && values.every(value => typeof value === 'boolean')) return 'boolean'
677
+ if (values.length > 0 && values.every(value => typeof value === 'number' && Number.isFinite(value))) return 'number'
678
+ return 'string'
679
+ }
680
+
681
+ /**
682
+ * A rejected edit plan used to come back as a bare message, so the caller had
683
+ * to replay the whole payload just to discover what it had sent (docs/issue/0011
684
+ * I8 / T7). Every validation failure now names the shape that was received and
685
+ * says that only the offending command has to change; `details` carries the
686
+ * same facts as data for callers that can read them.
687
+ */
688
+ export function withEditRetryContext(error: unknown, args: { workbookId: string; versions?: unknown; commands?: unknown }): DataSheetError {
689
+ if (error instanceof DataSheetError) return error
690
+ const message = error instanceof Error ? error.message : String(error)
691
+ const commandList = Array.isArray(args.commands) ? args.commands : []
692
+ const versionList = Array.isArray(args.versions) ? args.versions : []
693
+ const types = commandList.map(entry => (
694
+ typeof entry === 'object' && entry !== null ? String((entry as Record<string, unknown>).type ?? '?') : '?'
695
+ ))
696
+ const shape = versionList.length > 0
697
+ ? `${versionList.length} 个 version 分组`
698
+ : `${commandList.length} 条命令${types.length === 0 ? '' : `(${[...new Set(types)].join('、')})`}`
699
+ return new DataSheetError(
700
+ 'VALIDATION_ERROR',
701
+ `${message};已收参数:workbookId=${args.workbookId},${shape}。修正出错的那条命令后重试即可,不必重发整份 payload。`,
702
+ 400,
703
+ {
704
+ workbookId: args.workbookId,
705
+ versionCount: versionList.length,
706
+ commandCount: commandList.length,
707
+ commandTypes: [...new Set(types)],
708
+ },
709
+ )
710
+ }
711
+
712
+ export async function executeWorkbookEdit(args: {
713
+ workbookId: string
714
+ versions: unknown
715
+ commands: unknown
716
+ baseRevision?: number
717
+ opId?: string
718
+ intent?: string
719
+ agentRunId?: string
720
+ lineage?: LineageContext
721
+ dryRun: boolean
722
+ }): Promise<unknown> {
723
+ const warnings: string[] = []
724
+ let parsedVersions: ParsedEditVersion[]
725
+ try {
726
+ parsedVersions = await parseEditVersionGroups(args.versions, args.commands, args.intent, warnings, args.workbookId)
727
+ } catch (error) {
728
+ throw withEditRetryContext(error, args)
729
+ }
730
+ const meta = await readWorkbook(args.workbookId)
731
+ if (meta === null) throw new Error('工作簿不存在:' + args.workbookId)
732
+ const versions = bindEditVersionGroups(meta.snapshot, parsedVersions, args.workbookId)
733
+ collectDuplicateCellWrites(versions, warnings)
734
+ const commands = versions.flatMap(version => version.commands)
735
+ const importSteps = collectImportFileSteps(versions)
736
+ const currentRevision = meta.revision ?? 0
737
+ const steps = versions.map((version, index) => {
738
+ const operationId = args.opId === undefined ? undefined : versions.length === 1 ? args.opId : args.opId + ':version:' + (index + 1)
739
+ return {
740
+ type: 'batch' as const,
741
+ // Only the durable { type, payload } shape is persisted: the Agent-facing
742
+ // import receipt metadata must not change the Version/Step model or the
743
+ // request hash (and therefore opId idempotency).
744
+ payload: { operations: version.commands.map(command => ({ type: command.type, payload: command.payload })) },
745
+ opId: operationId,
746
+ versionId: newAgentVersionId(args.workbookId, operationId),
747
+ versionSummary: version.summary,
748
+ // One Agent tool call is a deliberate Version, not session churn (§4.2).
749
+ versionKind: 'named' as const,
750
+ versionIntent: version.intent,
751
+ agentRunId: args.agentRunId,
752
+ lineage: args.lineage,
753
+ }
754
+ })
755
+ // dryRun shares this exact call: the same opId checks, payload resolution,
756
+ // `applyWorkbookOperation` validation and revision accounting run before the
757
+ // manager decides whether to persist.
758
+ const plan = await mutateWorkbookPlan({
759
+ workbookId: args.workbookId,
760
+ actor: 'agent',
761
+ steps,
762
+ expectedRevision: args.baseRevision,
763
+ ...(args.dryRun ? { dryRun: true } : {}),
764
+ })
765
+ const committedVersions = plan.operations
766
+ const revisionAfter = plan.meta.revision ?? currentRevision
767
+ const last = committedVersions.at(-1)
768
+ const affected = summarizeAffectedSheets(versions, plan.meta.snapshot)
769
+ // A replayed plan wrote nothing this turn, so no receipt may be fabricated:
770
+ // the file on disk may have changed since the original write and the sheet
771
+ // still holds the earlier content. `replayed: true` tells the caller to read
772
+ // the workbook instead of trusting a proof from this turn.
773
+ const imports = plan.replayed === true ? [] : importSteps.map(step => importFileReceipt(step, {
774
+ workbookId: args.workbookId,
775
+ revisionBefore: currentRevision,
776
+ revisionAfter,
777
+ snapshotBefore: meta.snapshot,
778
+ snapshotAfter: plan.meta.snapshot,
779
+ }))
780
+ for (const receipt of imports) for (const warning of receipt.warnings) warnings.push(warning)
781
+ return {
782
+ ok: true,
783
+ ...(args.dryRun ? { dryRun: true } : {}),
784
+ workbookId: args.workbookId,
785
+ revision: args.dryRun ? currentRevision : revisionAfter,
786
+ revisionBefore: currentRevision,
787
+ revisionAfter,
788
+ versionCount: committedVersions.length,
789
+ commandCount: commands.length,
790
+ affectedSheets: affected.sheets,
791
+ sheetRefs: affected.refs,
792
+ affectedCellCount: affected.cellCount,
793
+ ...(imports.length === 0 ? {} : { imports }),
794
+ ...(warnings.length === 0 ? {} : { warnings }),
795
+ ...(plan.replayed === true ? { replayed: true } : {}),
796
+ ...(plan.transformed === true
797
+ ? { transformed: true, rebasedOperations: plan.rebasedOperations ?? [] }
798
+ : {}),
799
+ ...(args.intent === undefined ? {} : { intent: args.intent }),
800
+ ...(args.agentRunId === undefined ? {} : { agentRunId: args.agentRunId }),
801
+ ...(args.lineage === undefined ? {} : { lineage: args.lineage }),
802
+ versions: committedVersions.map((operation, index) => ({
803
+ ...operationReceipt(operation),
804
+ summary: versions[index]?.summary ?? operation.versionSummary ?? '',
805
+ ...(versions[index]?.intent === undefined ? {} : { intent: versions[index]?.intent }),
806
+ stepCount: versions[index]?.commands.length ?? workbookOperationSteps(operation).length,
807
+ commands: (versions[index]?.commands ?? []).map(command => ({ type: command.type, sheet: command.payload.sheetId ?? command.payload.sheetName ?? null })),
808
+ })),
809
+ ...(last === undefined ? {} : { operation: operationReceipt(last) }),
810
+ }
811
+ }
812
+
813
+ /**
814
+ * One `import_file` step after binding (its sheet reference is resolved). The
815
+ * command itself is already normalized to a durable `update_cells` payload.
816
+ */
817
+ export interface ImportFileStep {
818
+ type: WorkbookOperationType
819
+ payload: WorkbookOperationPayload
820
+ importFile: ImportFileMeta
821
+ }
822
+
823
+ export function collectImportFileSteps(versions: readonly ParsedEditVersion[]): ImportFileStep[] {
824
+ const steps: ImportFileStep[] = []
825
+ for (const version of versions) {
826
+ for (const command of version.commands) {
827
+ if (command.importFile === undefined) continue
828
+ steps.push({ type: command.type, payload: command.payload, importFile: command.importFile })
829
+ }
830
+ }
831
+ return steps
832
+ }
833
+
834
+ /** One Agent-facing import receipt for a single `import_file` command. */
835
+ export interface ImportFileReceipt {
836
+ workbookId: string
837
+ revisionBefore: number
838
+ revisionAfter: number
839
+ sheetId: string | null
840
+ range: WorkbookRange
841
+ rowsWritten: number
842
+ cellsWritten: number
843
+ sourceRowCount: number
844
+ truncated: boolean
845
+ sourcePath: string
846
+ rowCountBefore: number | null
847
+ rowCountAfter: number | null
848
+ capacityExpanded: boolean
849
+ firstRow: unknown[]
850
+ lastRow: unknown[]
851
+ valuesHash: string
852
+ warnings: string[]
853
+ }
854
+
855
+ /**
856
+ * Self-verifying import receipt: range/row/cell counts, source size, a content
857
+ * digest plus first/last row samples, and the auto-expansion result so the
858
+ * Agent never has to reread the whole range to prove the write landed.
859
+ */
860
+ export function importFileReceipt(step: ImportFileStep, context: {
861
+ workbookId: string
862
+ revisionBefore: number
863
+ revisionAfter: number
864
+ snapshotBefore: unknown
865
+ snapshotAfter: unknown
866
+ }): ImportFileReceipt {
867
+ const meta = step.importFile
868
+ const sheetId = step.payload.sheetId ?? null
869
+ const rowCountBefore = snapshotRowCapacity(context.snapshotBefore, sheetId)
870
+ const rowCountAfter = snapshotRowCapacity(context.snapshotAfter, sheetId)
871
+ const capacityExpanded = rowCountBefore !== null && rowCountAfter !== null && rowCountAfter > rowCountBefore
872
+ const receiptWarnings: string[] = []
873
+ if (capacityExpanded && meta.verifyCapacity) {
874
+ receiptWarnings.push(`import_file 自动扩容:${sheetId ?? '(Host 推断的 sheet)'} rowCount ${rowCountBefore} → ${rowCountAfter}(写入到第 ${meta.range.endRow + 1} 行),无需 insert_rows`)
875
+ }
876
+ if (meta.truncated) {
877
+ receiptWarnings.push(`import_file 按 allowTruncate 只导入了源文件 ${meta.sourceRowCount} 行中的前 ${meta.rowsWritten} 行,其余未写入`)
878
+ }
879
+ return {
880
+ workbookId: context.workbookId,
881
+ revisionBefore: context.revisionBefore,
882
+ revisionAfter: context.revisionAfter,
883
+ sheetId,
884
+ range: meta.range,
885
+ rowsWritten: meta.rowsWritten,
886
+ cellsWritten: meta.cellsWritten,
887
+ sourceRowCount: meta.sourceRowCount,
888
+ truncated: meta.truncated,
889
+ sourcePath: meta.sourcePath,
890
+ rowCountBefore,
891
+ rowCountAfter,
892
+ capacityExpanded,
893
+ firstRow: meta.firstRow,
894
+ lastRow: meta.lastRow,
895
+ valuesHash: meta.valuesHash,
896
+ warnings: receiptWarnings,
897
+ }
898
+ }
899
+
900
+ /** Physical row capacity (`rowCount`) of one sheet in a snapshot. */
901
+ export function snapshotRowCapacity(snapshot: unknown, sheetRef: string | null): number | null {
902
+ if (sheetRef === null) return null
903
+ try {
904
+ const sheet = snapshotSheet(snapshot, sheetRef)
905
+ return typeof sheet.rowCount === 'number' ? sheet.rowCount : null
906
+ } catch {
907
+ return null
908
+ }
909
+ }
910
+
911
+ /** Receipt data that lets an Agent prove what a write touched without rereading storage. */
912
+ export function summarizeAffectedSheets(versions: readonly ParsedEditVersion[], snapshot: unknown): { sheets: string[]; refs: Array<{ name: string; sheetId: string }>; cellCount: number } {
913
+ const names = new Map(sheetOverviews(snapshot).map(sheet => [sheet.id, sheet.name]))
914
+ const labels = new Set<string>()
915
+ const refs = new Map<string, { name: string; sheetId: string }>()
916
+ let cellCount = 0
917
+ for (const version of versions) {
918
+ for (const command of version.commands) {
919
+ const sheetId = command.payload.sheetId ?? command.payload.sheetName
920
+ const label = typeof sheetId === 'string' && sheetId.length > 0
921
+ ? `${names.get(sheetId) ?? sheetId}(${sheetId})`
922
+ : '(由 Host 推断)'
923
+ labels.add(label)
924
+ // The readable mapping is also published as data: sheet ids are hashes,
925
+ // and a caller that just created a sheet needs the id without parsing the
926
+ // "name(id)" label back out (docs/issue/0011 I6 / T7).
927
+ if (typeof sheetId === 'string' && sheetId.length > 0) {
928
+ refs.set(sheetId, { name: names.get(sheetId) ?? sheetId, sheetId })
929
+ }
930
+ const payload = command.payload
931
+ if (Array.isArray(payload.cells)) cellCount += payload.cells.length
932
+ else if (Array.isArray(payload.rows)) cellCount += payload.rows.length * (Array.isArray(payload.columns) ? payload.columns.length : 0)
933
+ else if (payload.range !== undefined) {
934
+ cellCount += (payload.range.endRow - payload.range.startRow + 1) * (payload.range.endColumn - payload.range.startColumn + 1)
935
+ }
936
+ }
937
+ }
938
+ return { sheets: [...labels], refs: [...refs.values()], cellCount }
939
+ }
940
+
941
+ export function newAgentVersionId(workbookId: string, opId?: string): string {
942
+ const suffix = opId === undefined
943
+ ? randomUUID().slice(0, 12)
944
+ : createHash('sha256').update(opId).digest('hex').slice(0, 16)
945
+ return `agent-version:${workbookId}:${suffix}`
946
+ }
947
+
948
+ export function parseStyle(value: unknown): NonNullable<WorkbookOperationPayload['style']> {
949
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error('style 必须是对象')
950
+ const raw = value as Record<string, unknown>
951
+ const style: NonNullable<WorkbookOperationPayload['style']> = {}
952
+ for (const key of ['ff', 'bg', 'cl', 'n'] as const) {
953
+ if (raw[key] !== undefined) {
954
+ if (typeof raw[key] !== 'string') throw new Error(`style.${key} 必须是字符串`)
955
+ style[key] = raw[key]
956
+ }
957
+ }
958
+ for (const key of ['fs', 'ht', 'vt', 'tr'] as const) {
959
+ if (raw[key] !== undefined) {
960
+ if (typeof raw[key] !== 'number' || !Number.isFinite(raw[key])) throw new Error(`style.${key} 必须是有限数字`)
961
+ style[key] = raw[key]
962
+ }
963
+ }
964
+ for (const key of ['bl', 'it', 'ul', 'st'] as const) {
965
+ if (raw[key] !== undefined) {
966
+ if (typeof raw[key] !== 'boolean') throw new Error(`style.${key} 必须是布尔值`)
967
+ style[key] = raw[key]
968
+ }
969
+ }
970
+ return style
971
+ }
972
+
973
+
974
+
975
+ export function parseSheetMeta(value: unknown): { source?: string; query?: string; generatedAt?: string; notes?: string } | undefined {
976
+ if (value === undefined || value === null) return undefined
977
+ if (typeof value !== 'object' || Array.isArray(value)) throw new Error('meta 必须是对象')
978
+ const raw = value as Record<string, unknown>
979
+ const meta: { source?: string; query?: string; generatedAt?: string; notes?: string } = {}
980
+ for (const key of ['source', 'query', 'generatedAt', 'notes'] as const) {
981
+ if (raw[key] !== undefined) {
982
+ if (typeof raw[key] !== 'string') throw new Error(`meta.${key} 必须是字符串`)
983
+ meta[key] = raw[key]
984
+ }
985
+ }
986
+ return meta
987
+ }