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