@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,230 @@
1
+ /**
2
+ * The only module that replaces a workbook's bytes (docs/issue/0022 P1).
3
+ *
4
+ * Every commit is a write-ahead journal plus temp-file renames, so an
5
+ * interrupted save is finished by the next read rather than leaving half a
6
+ * workbook on disk. It takes the journal lock from `store/read.ts` so a commit
7
+ * can never interleave with the recovery of another.
8
+ */
9
+ import { mkdir, rename, rm, unlink, writeFile } from 'node:fs/promises'
10
+ import { randomUUID } from 'node:crypto'
11
+ import { DEFAULT_WORKBOOK, type WorkbookMeta, type WorkbookWorktree } from '../types.ts'
12
+ import { normalizeWorkbookId, normalizeWorktreeId } from '../contract/ids.ts'
13
+ import { PROPOSAL_LOG_LIMIT, operationSummary } from '../contract/operations.ts'
14
+ import { validateSnapshotBounds } from '../contract/mutations.ts'
15
+ import { cloneSnapshot, normalizeSnapshotName } from '../contract/snapshots.ts'
16
+ import {
17
+ workbookCheckpointsPath,
18
+ workbookIndexPath,
19
+ workbookOperationsPath,
20
+ workbookPath,
21
+ workbookProposalsPath,
22
+ workbookRoot,
23
+ workbookSnapshotPath,
24
+ workbookTransactionPath,
25
+ workbookWorktreeDirectory,
26
+ workbookWorktreePath,
27
+ } from './paths.ts'
28
+ import {
29
+ fileExists,
30
+ readWorkbook,
31
+ recoverWorkbookTransactionNow,
32
+ summarize,
33
+ withWorkbookTransactionQueue,
34
+ workbookCache,
35
+ type StoredWorkbookTransaction,
36
+ type WorkbookIndex,
37
+ type WorkbookTransactionFile,
38
+ } from './read.ts'
39
+
40
+ /** Write one workbook meta atomically (creates the repo root on demand). */
41
+ export async function writeWorkbook(meta: WorkbookMeta, options: { persistSnapshot?: boolean; persistCheckpoints?: boolean } = {}): Promise<void> {
42
+ const normalizedId = normalizeWorkbookId(meta.id)
43
+ await withWorkbookTransactionQueue(normalizedId, async () => {
44
+ await mkdir(workbookRoot(), { recursive: true })
45
+ await recoverWorkbookTransactionNow(normalizedId)
46
+ // The DSH metadata name is the user-facing workbook name. Keep the Univer
47
+ // snapshot name aligned so exports and other consumers use the same name.
48
+ const normalizedMeta = { ...meta, id: normalizedId, snapshot: normalizeSnapshotName(meta.snapshot, meta.name) }
49
+ validateSnapshotBounds(normalizedMeta.snapshot)
50
+ const operationLog = normalizedMeta.operations ?? []
51
+ const proposalLog = normalizedMeta.proposals ?? []
52
+ // Versions are the durable audit trail. Keep the append-only operation log
53
+ // instead of truncating it to a recent tail so old versions can be replayed.
54
+ normalizedMeta.operations = operationLog
55
+ // The single place this bound is applied, since every write comes through here.
56
+ normalizedMeta.proposals = proposalLog.slice(-PROPOSAL_LOG_LIMIT)
57
+ const snapshotExists = await fileExists(workbookSnapshotPath(normalizedId))
58
+ const persistSnapshot = options.persistSnapshot === true || !snapshotExists
59
+ const checkpointsExist = await fileExists(workbookCheckpointsPath(normalizedId))
60
+ const snapshotRevision = persistSnapshot
61
+ ? normalizedMeta.revision ?? 0
62
+ : (typeof normalizedMeta.snapshotRevision === 'number' ? normalizedMeta.snapshotRevision : 0)
63
+ const {
64
+ snapshot: _snapshot,
65
+ checkpoints: _checkpoints,
66
+ operations: _operations,
67
+ proposals: _proposals,
68
+ ...metadata
69
+ } = normalizedMeta
70
+ const metadataWithSnapshotRevision = { ...metadata, snapshotRevision }
71
+ const files: WorkbookTransactionFile[] = [
72
+ { target: workbookOperationsPath(normalizedId), value: normalizedMeta.operations ?? [] },
73
+ { target: workbookProposalsPath(normalizedId), value: normalizedMeta.proposals ?? [] },
74
+ { target: workbookPath(normalizedId), value: metadataWithSnapshotRevision },
75
+ { target: workbookIndexPath(normalizedId), value: buildWorkbookIndex(normalizedMeta) },
76
+ ]
77
+ if (persistSnapshot) files.unshift({ target: workbookSnapshotPath(normalizedId), value: { revision: normalizedMeta.revision ?? 0, snapshot: normalizedMeta.snapshot } })
78
+ if (options.persistCheckpoints === true || !checkpointsExist) files.splice(persistSnapshot ? 1 : 0, 0, { target: workbookCheckpointsPath(normalizedId), value: { checkpoints: normalizedMeta.checkpoints ?? [] } })
79
+ await commitWorkbookTransaction(normalizedId, files, normalizedMeta.revision ?? 0)
80
+ workbookCache.set(normalizedId, {
81
+ meta: { ...normalizedMeta, snapshotRevision, checkpoints: normalizedMeta.checkpoints ?? [] },
82
+ checkpointsLoaded: options.persistCheckpoints === true || !checkpointsExist,
83
+ })
84
+ })
85
+ }
86
+
87
+ export async function writeJsonAtomically(path: string, value: unknown): Promise<void> {
88
+ const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`
89
+ try {
90
+ await writeFile(tempPath, JSON.stringify(value, null, 2), 'utf8')
91
+ await rename(tempPath, path)
92
+ } finally {
93
+ try { await unlink(tempPath) } catch { /* rename already removed it */ }
94
+ }
95
+ }
96
+
97
+ export async function commitWorkbookTransaction(id: string, files: WorkbookTransactionFile[], revision: number): Promise<void> {
98
+ const transactionId = randomUUID()
99
+ const journalPath = workbookTransactionPath(id)
100
+ const prepared = files.map(file => ({ target: file.target, temp: `${file.target}.${transactionId}.tmp` }))
101
+ let journalWritten = false
102
+ try {
103
+ for (const [index, file] of files.entries()) {
104
+ await writeFile(prepared[index]!.temp, JSON.stringify(file.value, null, 2), 'utf8')
105
+ }
106
+ await writeJsonAtomically(journalPath, { revision, files: prepared } satisfies StoredWorkbookTransaction)
107
+ journalWritten = true
108
+ for (const file of prepared) await rename(file.temp, file.target)
109
+ await unlink(journalPath)
110
+ } catch (error) {
111
+ // Once the journal exists, its prepared files are intentionally retained
112
+ // so the next read can complete the commit. Before that point cleanup is
113
+ // safe because no target has been replaced.
114
+ if (!journalWritten) {
115
+ await Promise.all(prepared.map(file => unlink(file.temp).catch(() => undefined)))
116
+ }
117
+ throw error
118
+ }
119
+ }
120
+
121
+ export async function writeWorktree(worktree: WorkbookWorktree): Promise<void> {
122
+ await mkdir(workbookWorktreeDirectory(worktree.workbookId), { recursive: true })
123
+ await writeJsonAtomically(workbookWorktreePath(worktree.workbookId, worktree.id), worktree)
124
+ }
125
+
126
+ export function buildWorkbookIndex(meta: WorkbookMeta): WorkbookIndex {
127
+ const summary = summarize(meta)
128
+ return {
129
+ id: summary.id,
130
+ name: summary.name,
131
+ sheetOrder: summary.sheets.map(sheet => sheet.id),
132
+ sheets: summary.sheets,
133
+ updatedAt: summary.updatedAt,
134
+ revision: meta.revision ?? 0,
135
+ operations: (meta.operations ?? []).map(operation => ({
136
+ opId: operation.opId,
137
+ workbookId: operation.workbookId,
138
+ revision: operation.revision,
139
+ actor: operation.actor,
140
+ type: operation.type,
141
+ timestamp: operation.timestamp,
142
+ summary: operationSummary(operation),
143
+ })),
144
+ proposals: (meta.proposals ?? []).map(({ snapshot: _snapshot, ...proposal }) => proposal),
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Delete a workbook and all of its Datasheet sidecars. `main` is the stable
150
+ * default workbook and is intentionally protected from accidental deletion.
151
+ * The queue makes deletion serialize with an in-flight save for the same id.
152
+ */
153
+ export async function deleteWorkbook(id: string): Promise<void> {
154
+ const normalizedId = normalizeWorkbookId(id)
155
+ if (normalizedId === DEFAULT_WORKBOOK) throw new Error('默认工作簿 main 不能删除')
156
+ await withWorkbookQueue(normalizedId, async () => {
157
+ workbookCache.delete(normalizedId)
158
+ const paths = [
159
+ workbookPath(normalizedId),
160
+ workbookIndexPath(normalizedId),
161
+ workbookSnapshotPath(normalizedId),
162
+ workbookCheckpointsPath(normalizedId),
163
+ workbookOperationsPath(normalizedId),
164
+ workbookProposalsPath(normalizedId),
165
+ workbookTransactionPath(normalizedId),
166
+ ]
167
+ for (const path of paths) {
168
+ try { await unlink(path) } catch (error) {
169
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
170
+ }
171
+ }
172
+ await rm(workbookWorktreeDirectory(normalizedId), { recursive: true, force: true })
173
+ })
174
+ }
175
+
176
+ /** Rename the user-facing workbook label without changing its stable id or
177
+ * history. The snapshot name is kept in sync by `writeWorkbook`. */
178
+ export async function renameWorkbook(id: string, name: string): Promise<WorkbookMeta> {
179
+ const normalizedId = normalizeWorkbookId(id)
180
+ const normalizedName = name.trim()
181
+ if (normalizedName.length === 0) throw new Error('工作簿名称不能为空')
182
+ return withWorkbookQueue(normalizedId, async () => {
183
+ const existing = await readWorkbook(normalizedId)
184
+ if (existing === null) throw new Error(`工作簿不存在:${id}`)
185
+ const meta = cloneSnapshot(existing) as WorkbookMeta
186
+ meta.name = normalizedName
187
+ meta.updatedAt = new Date().toISOString()
188
+ await writeWorkbook(meta)
189
+ return meta
190
+ })
191
+ }
192
+
193
+ /** Serialize mutations (and worktree writes) per id.
194
+ * `deleteWorkbook` and `renameWorkbook` take the same lock as `mutateWorkbook`,
195
+ * so a delete can never interleave with an in-flight save. The manager imports
196
+ * them from here rather than owning them, because the store must not depend on
197
+ * its callers. */
198
+
199
+ const mutationQueues = new Map<string, Promise<void>>()
200
+
201
+ const worktreeQueues = new Map<string, Promise<void>>()
202
+
203
+ export async function withWorkbookQueue<T>(id: string, task: () => Promise<T>): Promise<T> {
204
+ const previous = mutationQueues.get(id) ?? Promise.resolve()
205
+ let release!: () => void
206
+ const current = new Promise<void>(resolveRelease => { release = resolveRelease })
207
+ mutationQueues.set(id, current)
208
+ await previous
209
+ try {
210
+ return await task()
211
+ } finally {
212
+ release()
213
+ if (mutationQueues.get(id) === current) mutationQueues.delete(id)
214
+ }
215
+ }
216
+
217
+ export async function withWorktreeQueue<T>(workbookId: string, worktreeId: string, task: () => Promise<T>): Promise<T> {
218
+ const key = `${normalizeWorkbookId(workbookId)}:${normalizeWorktreeId(worktreeId)}`
219
+ const previous = worktreeQueues.get(key) ?? Promise.resolve()
220
+ let release!: () => void
221
+ const current = new Promise<void>(resolveRelease => { release = resolveRelease })
222
+ worktreeQueues.set(key, current)
223
+ await previous
224
+ try {
225
+ return await task()
226
+ } finally {
227
+ release()
228
+ if (worktreeQueues.get(key) === current) worktreeQueues.delete(key)
229
+ }
230
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Reading tool arguments (docs/issue/0022 P3b).
3
+ *
4
+ * The Agent hands a tool an untyped record; every field has to be validated
5
+ * before it can become part of an operation. These are the conversions several
6
+ * tools share — the ones only one tool needs live in that tool.
7
+ */
8
+ import { SHEET_MAX_ROWS, type WorkbookCellDeleteShift } from '../types.ts'
9
+ import { normalizeWorkbookId } from '../contract/ids.ts'
10
+ import { parseIndex } from '../request-params.ts'
11
+
12
+ export type ToolArguments = Record<string, unknown>
13
+
14
+ export function asToolArguments(value: unknown, label = '参数'): ToolArguments {
15
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error(label + '必须是对象')
16
+ return value as ToolArguments
17
+ }
18
+
19
+ /** Validate and normalize a workbook id argument. There is no implicit
20
+ * `main`: every Agent call must identify the workbook it intends to change. */
21
+ export function workbookIdOf(raw: unknown): string {
22
+ if (typeof raw !== 'string' || raw.trim().length === 0) {
23
+ throw new Error('必须明确指定 workbookId;请先调用 workbook_catalog(action=list) 获取工作簿 id')
24
+ }
25
+ return normalizeWorkbookId(raw)
26
+ }
27
+
28
+ /** ISO timestamp helper. */
29
+ export function now(): string {
30
+ return new Date().toISOString()
31
+ }
32
+
33
+ export function optionalIntent(value: unknown, name = 'intent'): string | undefined {
34
+ if (value === undefined || value === null) return undefined
35
+ if (typeof value !== 'string' || value.trim().length === 0 || value.trim().length > 280) throw new Error(`${name} 必须是 1 到 280 字符的字符串`)
36
+ return value.trim()
37
+ }
38
+
39
+ export function optionalAuditText(value: unknown, name: string, maxLength: number): string | undefined {
40
+ if (value === undefined || value === null) return undefined
41
+ if (typeof value !== 'string' || value.trim().length === 0 || value.trim().length > maxLength) throw new Error(`${name} 必须是 1 到 ${maxLength} 字符的字符串`)
42
+ return value.trim()
43
+ }
44
+
45
+ export function optionalString(value: unknown): string | undefined {
46
+ if (value === undefined || value === null) return undefined
47
+ if (typeof value !== 'string' || value.trim().length === 0) throw new Error('opId 必须是非空字符串')
48
+ return value.trim()
49
+ }
50
+
51
+ export function parseCount(value: unknown): number {
52
+ const count = parseIndex(value, 'count')
53
+ if (count < 1 || count > SHEET_MAX_ROWS) throw new Error(`count 必须是 1 到 ${SHEET_MAX_ROWS} 之间的整数`)
54
+ return count
55
+ }
56
+
57
+ /**
58
+ * `verifyHeader` defaults to true because comparing header labels is the only
59
+ * guard that catches a column order or rename mistake before rows are written.
60
+ * Only an explicit `false` relaxes it.
61
+ */
62
+ export function parseHeaderVerification(value: unknown, label: string): boolean {
63
+ if (value === undefined || value === null) return true
64
+ if (typeof value !== 'boolean') throw new Error(`${label} 必须是布尔值`)
65
+ return value
66
+ }
67
+
68
+ export function parseCellDeleteShift(value: unknown): WorkbookCellDeleteShift {
69
+ if (value === undefined || value === null || value === '') return 'clear'
70
+ if (value === 'clear' || value === 'left' || value === 'up') return value
71
+ throw new Error('shift 必须是 clear、left 或 up')
72
+ }
73
+
74
+ export function commandPayload(raw: ToolArguments): ToolArguments {
75
+ const nested = raw.payload
76
+ if (nested === undefined || nested === null) return raw
77
+ if (typeof nested !== 'object' || Array.isArray(nested)) throw new Error('command.payload 必须是对象')
78
+ // Permit callers to send the durable { type, payload } shape while keeping
79
+ // the documented flat command shape. Explicit top-level fields win.
80
+ return { ...(nested as ToolArguments), ...raw }
81
+ }
82
+
83
+ /**
84
+ * Accept the vocabulary used by both the public Agent tool and the Univer
85
+ * facade. Older callers use `sheet`, while generated calls often use the
86
+ * persisted field name `sheetId` or the human-facing `sheetName`.
87
+ */
88
+ export function sheetRefFromArguments(raw: ToolArguments, label = 'sheet'): string | undefined {
89
+ const candidates = ['sheet', 'sheetId', 'sheetName', 'sheet_id', 'sheet_name']
90
+ .filter(key => raw[key] !== undefined && raw[key] !== null && raw[key] !== '')
91
+ if (candidates.length === 0) return undefined
92
+ const refs = candidates.map(key => parseNonEmptyString(raw[key], `${label}.${key}`))
93
+ const first = refs[0] as string
94
+ if (refs.some(ref => ref !== first)) throw new Error(`${label}、sheetId、sheetName 只能指定同一个 sheet`)
95
+ return first
96
+ }
97
+
98
+ /** Resolve an optional Agent sheet reference. A missing reference is safe only
99
+ * when the workbook has one sheet; otherwise returning the first sheet would
100
+ * make a write silently target the wrong tab. */
101
+ export function sheetRefForSnapshot(snapshot: unknown, raw: ToolArguments, label: string): string {
102
+ const explicit = sheetRefFromArguments(raw, label)
103
+ if (explicit !== undefined) return explicit
104
+ const root = asToolArguments(snapshot, 'workbook snapshot')
105
+ const order = Array.isArray(root.sheetOrder) ? root.sheetOrder.filter(item => typeof item === 'string') as string[] : []
106
+ if (order.length === 1 && order[0] !== undefined) return order[0]
107
+ const sheets = typeof root.sheets === 'object' && root.sheets !== null && !Array.isArray(root.sheets)
108
+ ? Object.entries(root.sheets as Record<string, unknown>).map(([id, value]) => {
109
+ const sheet = value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}
110
+ return `${id}${typeof sheet.name === 'string' ? `(${sheet.name})` : ''}`
111
+ })
112
+ : []
113
+ throw new Error(`${label} 缺少 sheet;当前工作簿有多个 sheet,请使用 sheetId 或 sheetName:${sheets.join('、')}`)
114
+ }
115
+
116
+ export function commandSheet(raw: ToolArguments, index: number, _optional = false): string | undefined {
117
+ return sheetRefFromArguments(raw, 'commands[' + index + '].sheet')
118
+ }
119
+
120
+ export function commandIndex(raw: ToolArguments, index: number, field: 'index' | 'startRow' | 'startColumn'): number {
121
+ const value = raw[field] ?? (field === 'startRow' ? raw.index : field === 'startColumn' ? raw.index : undefined)
122
+ return parseIndex(value, 'commands[' + index + '].' + field)
123
+ }
124
+
125
+ export function parseNonEmptyString(value: unknown, label: string): string {
126
+ if (typeof value !== 'string' || value.trim().length === 0) throw new Error(`${label} 必须是非空字符串`)
127
+ return value.trim()
128
+ }
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Payload vocabulary shared by the two tools that write to a workbook
3
+ * (docs/issue/0022 P3b).
4
+ *
5
+ * `workbook_edit` and `univer_api` both accept a value matrix plus a range, both
6
+ * accept cells whose value comes from a file, both declare a table's columns, and
7
+ * both size a row or column. That translation lives here rather than in either
8
+ * tool: a tool importing the other tool is exactly the coupling this phase
9
+ * exists to prevent, and every helper here is about building a payload, not about
10
+ * which endpoint received it.
11
+ */
12
+ import { statSync } from 'node:fs'
13
+ import { readFile } from 'node:fs/promises'
14
+ import { resolve } from 'node:path'
15
+ import {
16
+ SHEET_MAX_COLUMNS,
17
+ SHEET_MAX_NAME,
18
+ SHEET_MAX_ROWS,
19
+ type SheetColumn,
20
+ type SheetColumnType,
21
+ type WorkbookCellUpdate,
22
+ type WorkbookOperationPayload,
23
+ type WorkbookRange,
24
+ } from '../types.ts'
25
+ import { parseA1Range, parseIndex, parseRange } from '../request-params.ts'
26
+ import { asToolArguments, parseNonEmptyString, type ToolArguments } from './arguments.ts'
27
+
28
+
29
+ /** Sheet column types a modelled table may declare. */
30
+ const COLUMN_TYPES: readonly SheetColumnType[] = ['string', 'number', 'boolean', 'date', 'currency']
31
+
32
+ export function parseCells(value: unknown): WorkbookCellUpdate[] {
33
+ if (!Array.isArray(value) || value.length === 0) throw new Error('cells 必须是非空数组')
34
+ if (value.length > SHEET_MAX_ROWS * SHEET_MAX_COLUMNS) throw new Error('一次修改的单元格数量超过上限')
35
+ return value.map((item, index) => {
36
+ if (typeof item !== 'object' || item === null || Array.isArray(item)) throw new Error(`第 ${index + 1} 个 cell 不是对象`)
37
+ const raw = item as Record<string, unknown>
38
+ const cellValue = raw.value
39
+ if (cellValue !== null && typeof cellValue !== 'string' && typeof cellValue !== 'number' && typeof cellValue !== 'boolean') {
40
+ throw new Error(`第 ${index + 1} 个 cell 的 value 必须是 string/number/boolean/null`)
41
+ }
42
+ if (typeof cellValue === 'number' && !Number.isFinite(cellValue)) throw new Error(`第 ${index + 1} 个 cell 的 value 不是有限数字`)
43
+ if (raw.formula !== undefined && typeof raw.formula !== 'string') throw new Error(`第 ${index + 1} 个 cell 的 formula 必须是字符串`)
44
+ return {
45
+ row: parseIndex(raw.row, `cells[${index}].row`),
46
+ column: parseIndex(raw.column, `cells[${index}].column`),
47
+ value: cellValue as WorkbookCellUpdate['value'],
48
+ ...(raw.formula === undefined ? {} : { formula: raw.formula }),
49
+ }
50
+ })
51
+ }
52
+
53
+ /**
54
+ * Resolve `cells[].valueFromFile` into a literal value (docs/issue/0011 I4 / T7).
55
+ *
56
+ * Long text had to be inlined into the tool call, and the same SQL was retyped
57
+ * into every workbook that needed it. A cell may now name a local text file
58
+ * instead. The content is read here and handed to the ordinary `parseCells`
59
+ * validation, so nothing bypasses the existing cell limits.
60
+ */
61
+ export async function resolveCellFileValues(value: unknown[], label: string): Promise<unknown[]> {
62
+ return Promise.all(value.map(async (item, index) => {
63
+ if (typeof item !== 'object' || item === null || Array.isArray(item)) return item
64
+ const raw = item as Record<string, unknown>
65
+ if (raw.valueFromFile === undefined || raw.valueFromFile === null) return item
66
+ const path = resolve(parseNonEmptyString(raw.valueFromFile, `${label}.cells[${index}].valueFromFile`))
67
+ let info: { size: number; isFile(): boolean }
68
+ try { info = statSync(path) } catch { throw new Error(`${label}.cells[${index}].valueFromFile 文件不存在:${path}`) }
69
+ if (!info.isFile()) throw new Error(`${label}.cells[${index}].valueFromFile 不是文件:${path}`)
70
+ if (info.size > TEXT_CELL_MAX_BYTES) {
71
+ throw new Error(`${label}.cells[${index}].valueFromFile 文件过大(${info.size} 字节,上限 ${TEXT_CELL_MAX_BYTES})`)
72
+ }
73
+ const content = await readFile(path, 'utf8')
74
+ const { valueFromFile: _consumed, ...rest } = raw
75
+ // An explicit `value` still wins, so the field is a fallback rather than an
76
+ // override that could silently disagree with the caller.
77
+ return raw.value === undefined ? { ...rest, value: content } : rest
78
+ }))
79
+ }
80
+
81
+ /**
82
+ * Resolve the target range of a value matrix.
83
+ *
84
+ * The matrix size is unambiguous, so it is the authority: `range` only needs
85
+ * to anchor the write, and a `range` whose declared end disagrees with
86
+ * `values` is corrected and reported as a warning instead of failing a large
87
+ * batch. End coordinates are inclusive, which the tool description now states.
88
+ */
89
+ export function parseValueRange(value: unknown, matrix: unknown[][], label: string, warnings: string[] = []): WorkbookRange {
90
+ const rowCount = matrix.length
91
+ const columnCount = matrix[0]?.length ?? 0
92
+ const derived = (startRow: number, startColumn: number): WorkbookRange =>
93
+ parseRange({ startRow, startColumn, endRow: startRow + rowCount - 1, endColumn: startColumn + columnCount - 1 })
94
+ if (value === undefined || value === null) return derived(1, 0)
95
+
96
+ let startRow: number
97
+ let startColumn: number
98
+ let declaredEndRow: number | undefined
99
+ let declaredEndColumn: number | undefined
100
+ if (typeof value === 'string') {
101
+ const parsed = parseA1Range(value)
102
+ startRow = parsed.startRow
103
+ startColumn = parsed.startColumn
104
+ declaredEndRow = parsed.endRow
105
+ declaredEndColumn = parsed.endColumn
106
+ } else {
107
+ const raw = asToolArguments(value, label + '.range')
108
+ startRow = raw.startRow === undefined ? 1 : parseIndex(raw.startRow, label + '.range.startRow')
109
+ startColumn = raw.startColumn === undefined ? 0 : parseIndex(raw.startColumn, label + '.range.startColumn')
110
+ declaredEndRow = raw.endRow === undefined ? undefined : parseIndex(raw.endRow, label + '.range.endRow')
111
+ declaredEndColumn = raw.endColumn === undefined ? undefined : parseIndex(raw.endColumn, label + '.range.endColumn')
112
+ }
113
+
114
+ const target = derived(startRow, startColumn)
115
+ if (declaredEndRow === undefined && declaredEndColumn === undefined) return target
116
+ if (declaredEndRow === target.endRow && declaredEndColumn === target.endColumn) return target
117
+ // Supplying only the range origin is enough. A declared end that disagrees
118
+ // is downgraded to a warning: the values matrix already fixes the extent.
119
+ const declared = { endRow: declaredEndRow ?? target.endRow, endColumn: declaredEndColumn ?? target.endColumn }
120
+ if (declared.endRow < target.startRow || declared.endColumn < target.startColumn) {
121
+ throw new Error(`${label}.range 结束位置不能小于开始位置:range 为 rows ${startRow}-${declared.endRow} × cols ${startColumn}-${declared.endColumn}`)
122
+ }
123
+ const declaredRows = declared.endRow - target.startRow + 1
124
+ const declaredColumns = declared.endColumn - target.startColumn + 1
125
+ warnings.push(
126
+ `${label}.range 与 values 尺寸不一致:range 为 ${declaredRows}×${declaredColumns}(rows ${startRow}-${declared.endRow} × cols ${startColumn}-${declared.endColumn}),values 为 ${rowCount}×${columnCount};已按 values 落位为 rows ${target.startRow}-${target.endRow} × cols ${target.startColumn}-${target.endColumn}(end 坐标 inclusive)`,
127
+ )
128
+ return target
129
+ }
130
+
131
+ export function parseApiValues(matrix: unknown[][], range: WorkbookRange): WorkbookCellUpdate[] {
132
+ const rowCount = range.endRow - range.startRow + 1
133
+ const columnCount = range.endColumn - range.startColumn + 1
134
+ if (matrix.length !== rowCount) throw new Error(`values 行数必须为 ${rowCount}`)
135
+ const cells: WorkbookCellUpdate[] = []
136
+ for (const [rowOffset, line] of matrix.entries()) {
137
+ if (line.length !== columnCount) throw new Error(`values[${rowOffset}] 列数必须为 ${columnCount}`)
138
+ for (const [columnOffset, cell] of line.entries()) {
139
+ if (cell !== null && typeof cell !== 'string' && typeof cell !== 'number' && typeof cell !== 'boolean') {
140
+ throw new Error(`values[${rowOffset}][${columnOffset}] 必须是 string/number/boolean/null`)
141
+ }
142
+ if (typeof cell === 'number' && !Number.isFinite(cell)) throw new Error('values 不能包含非有限数字')
143
+ cells.push({ row: range.startRow + rowOffset, column: range.startColumn + columnOffset, value: cell as WorkbookCellUpdate['value'] })
144
+ }
145
+ }
146
+ return cells
147
+ }
148
+
149
+ /** A single cell of long text (typically a SQL body) read from a local file. */
150
+ export const TEXT_CELL_MAX_BYTES = 1024 * 1024
151
+
152
+ export function parseFreeze(value: unknown): NonNullable<WorkbookOperationPayload['freeze']> {
153
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error('freeze 必须是对象')
154
+ const raw = value as Record<string, unknown>
155
+ const rows = raw.rows === undefined ? 0 : parseIndex(raw.rows, 'freeze.rows')
156
+ const columns = raw.columns === undefined ? 0 : parseIndex(raw.columns, 'freeze.columns')
157
+ return { rows, columns }
158
+ }
159
+
160
+ export function parseSort(value: unknown): NonNullable<WorkbookOperationPayload['sort']> {
161
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error('sort 必须是对象')
162
+ const raw = value as Record<string, unknown>
163
+ const direction = raw.direction === undefined ? 'asc' : raw.direction
164
+ if (direction !== 'asc' && direction !== 'desc') throw new Error('sort.direction 必须是 asc 或 desc')
165
+ return { column: parseIndex(raw.column, 'sort.column'), direction }
166
+ }
167
+
168
+ export interface ValidatedSheet {
169
+ name?: string
170
+ columns: SheetColumn[]
171
+ rows: Array<Record<string, unknown>>
172
+ }
173
+
174
+ export function cellValue(type: SheetColumnType, value: unknown): number | string | boolean {
175
+ switch (type) {
176
+ case 'number':
177
+ case 'currency': {
178
+ const numeric = typeof value === 'number' ? value : Number(value)
179
+ return Number.isFinite(numeric) ? numeric : String(value)
180
+ }
181
+ case 'boolean':
182
+ return typeof value === 'boolean' ? value : String(value) === 'true'
183
+ case 'date':
184
+ return value instanceof Date ? value.toISOString().slice(0, 10) : String(value)
185
+ default:
186
+ return String(value)
187
+ }
188
+ }
189
+
190
+ /** Validate one sheet argument; throws a model-facing message on bad input. */
191
+ export function validateSheet(args: { name: unknown; columns?: unknown; rows: unknown }): ValidatedSheet {
192
+ const name = args.name === undefined || args.name === null ? undefined : parseNonEmptyString(args.name, 'name')
193
+ if (name !== undefined && name.length > SHEET_MAX_NAME) throw new Error(`表格名称过长(最多 ${SHEET_MAX_NAME} 字符)`)
194
+ if (args.columns !== undefined && !Array.isArray(args.columns)) throw new Error('columns 必须是数组;已有 sheet 可省略,Host 会从表头推断')
195
+ const rawColumns = Array.isArray(args.columns) ? args.columns : []
196
+ if (rawColumns.length === 0 && !Array.isArray(args.rows)) throw new Error('至少需要一列')
197
+ if (rawColumns.length > SHEET_MAX_COLUMNS) throw new Error(`列数超过上限(最多 ${SHEET_MAX_COLUMNS} 列)`)
198
+ const keys = new Set<string>()
199
+ const columns: SheetColumn[] = rawColumns.map((column, index) => {
200
+ // Column-definition errors have to state the accepted structure and echo
201
+ // what arrived: "列定义必须是对象" alone cost a debugging round in the
202
+ // original session (docs/issue/0011 I7 / T6).
203
+ if (typeof column !== 'object' || column === null || Array.isArray(column)) {
204
+ throw new Error(`列定义必须是对象:columns[${index}] 期望 { key, label, type },实际收到 ${JSON.stringify(column) ?? String(column)}`)
205
+ }
206
+ const raw = column as Record<string, unknown>
207
+ if (typeof raw.key !== 'string' || !/^[A-Za-z0-9_]+$/.test(raw.key)) {
208
+ throw new Error(`列 key 必须是仅含字母、数字、下划线的非空字符串:columns[${index}] 期望 { key, label, type },实际收到 ${JSON.stringify(raw)}`)
209
+ }
210
+ if (keys.has(raw.key)) throw new Error(`列 key 重复:${raw.key}(columns[${index}])`)
211
+ keys.add(raw.key)
212
+ if (typeof raw.label !== 'string' || raw.label.length === 0) {
213
+ throw new Error(`列 ${raw.key} 缺少 label:期望 { key: ${JSON.stringify(raw.key)}, label: string, type: string },实际收到 ${JSON.stringify(raw)}`)
214
+ }
215
+ if (!COLUMN_TYPES.includes(raw.type as SheetColumnType)) {
216
+ throw new Error(`列 ${raw.key} 类型未知:${String(raw.type)};type 只能是 ${COLUMN_TYPES.join('、')}`)
217
+ }
218
+ return { key: raw.key, label: raw.label, type: raw.type as SheetColumnType }
219
+ })
220
+ if (!Array.isArray(args.rows)) throw new Error('rows 必须是数组')
221
+ if (args.rows.length > SHEET_MAX_ROWS) {
222
+ throw new Error(`行数超过上限(最多 ${SHEET_MAX_ROWS} 行);请使用分页查询或拆成多个 workbook`)
223
+ }
224
+ const rows = args.rows.every(row => Array.isArray(row))
225
+ ? args.rows.map((row, index) => {
226
+ if (columns.length === 0) throw new Error('rows 使用二维数组时必须同时提供 columns')
227
+ const values = row as unknown[]
228
+ if (values.length > columns.length) throw new Error(`第 ${index + 1} 行列数超过 columns 定义`)
229
+ return Object.fromEntries(columns.map((column, columnIndex) => [column.key, values[columnIndex] ?? null]))
230
+ })
231
+ : args.rows.map((row, index) => {
232
+ if (typeof row !== 'object' || row === null || Array.isArray(row)) {
233
+ throw new Error(`第 ${index + 1} 行不是对象`)
234
+ }
235
+ return row as Record<string, unknown>
236
+ })
237
+ return { name, columns, rows }
238
+ }
239
+
240
+ export function chunkRows(rows: Array<Record<string, unknown>>, size: number): Array<Array<Record<string, unknown>>> {
241
+ const chunks: Array<Array<Record<string, unknown>>> = []
242
+ for (let start = 0; start < rows.length; start += size) chunks.push(rows.slice(start, start + size))
243
+ return chunks
244
+ }
245
+
246
+ export function dimensionRange(index: number, dimension: 'row' | 'column'): WorkbookRange {
247
+ return dimension === 'column'
248
+ ? { startRow: 0, startColumn: index, endRow: 0, endColumn: index }
249
+ : { startRow: index, startColumn: 0, endRow: index, endColumn: 0 }
250
+ }
251
+
252
+ /**
253
+ * Row/column structural commands accept either an explicit `range` (matching
254
+ * the documented command vocabulary) or the older `index`/`startRow` +
255
+ * `count` pair. Missing position arguments report the full accepted contract
256
+ * instead of the single `startRow` field the old implementation demanded.
257
+ */
258
+ export function dimensionCommandRange(raw: ToolArguments, index: number, dimension: 'row' | 'column'): WorkbookRange {
259
+ const label = `commands[${index}]`
260
+ const positionField = dimension === 'row' ? 'startRow' : 'startColumn'
261
+ const endField = dimension === 'row' ? 'endRow' : 'endColumn'
262
+ const explicit = raw.range
263
+ if (typeof explicit === 'string') {
264
+ const parsed = parseA1Range(explicit)
265
+ return dimension === 'row'
266
+ ? { startRow: parsed.startRow, startColumn: 0, endRow: parsed.endRow, endColumn: 0 }
267
+ : { startRow: 0, startColumn: parsed.startColumn, endRow: 0, endColumn: parsed.endColumn }
268
+ }
269
+ const range = explicit === undefined || explicit === null ? undefined : asToolArguments(explicit, `${label}.range`)
270
+ const startValue = range?.[positionField] ?? raw[positionField] ?? raw.index
271
+ if (startValue === undefined || startValue === null) {
272
+ throw new Error(`${label} 缺少${dimension === 'row' ? '行' : '列'}位置:请传 ${label}.range:{${positionField},${endField}}(0-based,结束坐标 inclusive),或传 ${label}.index / ${label}.${positionField} 搭配 count`)
273
+ }
274
+ const start = parseIndex(startValue, `${label}.${positionField}`)
275
+ const rawEnd = range?.[endField]
276
+ const end = rawEnd === undefined || rawEnd === null ? start : parseIndex(rawEnd, `${label}.${endField}`)
277
+ if (end < start) throw new Error(`${label}.range 结束位置不能小于开始位置(${start} > ${end})`)
278
+ return dimension === 'row'
279
+ ? { startRow: start, startColumn: 0, endRow: end, endColumn: 0 }
280
+ : { startRow: 0, startColumn: start, endRow: 0, endColumn: end }
281
+ }
282
+
283
+ export function parseFiniteNumber(value: unknown, label: string): number {
284
+ if (typeof value !== 'number' || !Number.isFinite(value)) throw new Error(`${label} 必须是有限数字`)
285
+ return value
286
+ }
287
+
288
+ export function parseHyperlink(value: unknown): string {
289
+ if (typeof value !== 'string' || value.trim().length === 0) throw new Error('url 必须是非空字符串')
290
+ const url = value.trim()
291
+ if (!/^(?:https?:|mailto:|file:|#)/i.test(url)) throw new Error('url 只支持 http(s)、mailto、file 或 sheet 内部链接')
292
+ return url
293
+ }
294
+
295
+ export function parseHyperlinkLabel(value: unknown): string {
296
+ if (typeof value !== 'string') throw new Error('label 必须是字符串')
297
+ return value
298
+ }