@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,364 @@
1
+ /**
2
+ * HTTP transport for the Datasheet plugin (docs/issue/0022 P3).
3
+ *
4
+ * This module does two things and nothing else: it adapts the plugin's routes to
5
+ * whichever carrier is running (the DSH Connection fetch carrier, or the
6
+ * Electron shell's webserver), and it decides which route a
7
+ * `/data-sheet/workbook/...` path is. The handlers live in the domain modules
8
+ * beside it, so a new endpoint means one case here and one block in a handler.
9
+ */
10
+ import type { IncomingMessage, ServerResponse } from 'node:http'
11
+ import type { Context } from '@deepseek-ai/cordis'
12
+ import type { ConnectionFetchRoute } from '@deepseek-ai/dsh-client-connection'
13
+ import { DataSheetError } from '../errors.ts'
14
+ import { WorkbookRevisionConflictError } from '../workbook-manager.ts'
15
+ import { parseIndex } from '../request-params.ts'
16
+
17
+ /**
18
+ * Structural view of the host webserver route API (no manifest dependency).
19
+ *
20
+ * Declared here rather than in the plugin body because this is where the cast
21
+ * that needs it lives: `registerConnectionRoutes` registers the same handlers
22
+ * with both carriers.
23
+ */
24
+ interface WebServerLike {
25
+ register(route: {
26
+ kind: 'exact' | 'prefix'
27
+ path: string
28
+ handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>
29
+ }): () => void
30
+ }
31
+
32
+ declare module '@deepseek-ai/cordis' {
33
+ interface Context {
34
+ webServer: WebServerLike
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Describe a failed route, naming the path that failed.
40
+ *
41
+ * @param where - The path or prefix being served.
42
+ * @param error - Whatever was thrown.
43
+ * @returns a message a person can act on.
44
+ */
45
+ function described(where: string, error: unknown): string {
46
+ return `${where} 处理失败:${error instanceof Error ? error.message : String(error)}`
47
+ }
48
+
49
+ export interface LegacyRoute {
50
+ readonly kind: 'exact' | 'prefix'
51
+ readonly path: string
52
+ readonly handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>
53
+ }
54
+
55
+ export interface LegacyResponse {
56
+ writeHead(status: number, headers?: Record<string, string>): void
57
+ write(chunk: string | Uint8Array): boolean
58
+ end(chunk?: string | Uint8Array): void
59
+ once(event: 'close', listener: () => void): void
60
+ on(event: 'drain', listener: () => void): void
61
+ destroy(): void
62
+ }
63
+
64
+ export function routeFor(routes: readonly LegacyRoute[], pathname: string): LegacyRoute | undefined {
65
+ return routes.find(route => route.kind === 'exact' && route.path === pathname)
66
+ ?? routes
67
+ .filter(route => route.kind === 'prefix' && (pathname === route.path || pathname.startsWith(`${route.path}/`)))
68
+ .sort((left, right) => right.path.length - left.path.length)[0]
69
+ }
70
+
71
+ /** Adapt the HTTP handlers to the official Electron shell's Connection carrier. */
72
+ export async function dispatchConnectionRoute(routes: readonly LegacyRoute[], request: Request, prefix: string): Promise<Response> {
73
+ const carrierUrl = new URL(request.url)
74
+ const rawPath = carrierUrl.searchParams.get('path')
75
+ if (rawPath === null || !rawPath.startsWith(`${prefix}/`)) return new Response('not found', { status: 404 })
76
+ const target = new URL(rawPath, 'http://dsh.invalid')
77
+ const route = routeFor(routes, target.pathname)
78
+ if (route === undefined) return new Response('not found', { status: 404 })
79
+ const body = request.body === null ? undefined : Buffer.from(await request.arrayBuffer())
80
+ let started = false
81
+ let status = 200
82
+ let headers: Record<string, string> = {}
83
+ let start!: () => void
84
+ const startedPromise = new Promise<void>(resolve => { start = resolve })
85
+ let streamController: ReadableStreamDefaultController<Uint8Array> | undefined
86
+ let closeListener: (() => void) | undefined
87
+ const begin = (nextStatus = 200, nextHeaders?: Record<string, string>): void => {
88
+ if (started) return
89
+ started = true
90
+ status = nextStatus
91
+ headers = nextHeaders ?? {}
92
+ start()
93
+ }
94
+ const response: LegacyResponse = {
95
+ writeHead(nextStatus, nextHeaders = {}) { begin(nextStatus, nextHeaders) },
96
+ write(chunk) {
97
+ begin()
98
+ try { streamController?.enqueue(typeof chunk === 'string' ? Buffer.from(chunk) : chunk) } catch { return false }
99
+ return (streamController?.desiredSize ?? 1) > 0
100
+ },
101
+ end(chunk) {
102
+ begin()
103
+ try {
104
+ if (chunk !== undefined) streamController?.enqueue(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)
105
+ streamController?.close()
106
+ } catch { /* the browser cancelled the stream */ }
107
+ },
108
+ once(_event, listener) { closeListener = listener },
109
+ on() { /* node:http backpressure is not needed by the Fetch stream */ },
110
+ destroy() { closeListener?.(); try { streamController?.close() } catch { /* already closed */ } },
111
+ }
112
+ const nodeHeaders = Object.fromEntries(request.headers)
113
+ nodeHeaders.host ??= '127.0.0.1'
114
+ const nodeRequest = {
115
+ method: request.method,
116
+ url: `${target.pathname}${target.search}`,
117
+ headers: nodeHeaders,
118
+ socket: { localAddress: '127.0.0.1' },
119
+ async *[Symbol.asyncIterator](): AsyncGenerator<Buffer> {
120
+ if (body !== undefined && body.byteLength > 0) yield body
121
+ },
122
+ } as unknown as IncomingMessage
123
+ const stream = new ReadableStream<Uint8Array>({
124
+ start(controller) {
125
+ streamController = controller
126
+ void route.handler(nodeRequest, response as unknown as ServerResponse).catch(error => {
127
+ if (!started) begin(500, { 'content-type': 'application/json; charset=utf-8' })
128
+ response.end(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }))
129
+ })
130
+ },
131
+ cancel() { closeListener?.() },
132
+ })
133
+ await startedPromise
134
+ return new Response(stream, { status, headers })
135
+ }
136
+
137
+ export function registerConnectionRoutes(ctx: Context, routes: readonly LegacyRoute[], prefix: string): void {
138
+ const route: ConnectionFetchRoute = {
139
+ path: `/api/${prefix.slice(1).split('/', 1)[0]}`,
140
+ methods: ['GET', 'HEAD', 'POST'],
141
+ requestBody: 'buffered',
142
+ // A failed handler must say so. DSH's web server turns an exception into a bodyless 400,
143
+ // which is indistinguishable from a malformed request — the same blindness that made a
144
+ // missing declaration look like a broken canvas.
145
+ fetch: async request => {
146
+ try {
147
+ return await dispatchConnectionRoute(routes, request, prefix)
148
+ } catch (error) {
149
+ return new Response(JSON.stringify({ error: described(prefix, error) }), {
150
+ status: 500,
151
+ headers: { 'content-type': 'application/json' },
152
+ })
153
+ }
154
+ },
155
+ }
156
+ ctx.effect(() => ctx.connection.fetch.register(route), `data-sheet: ${route.path} Connection route`)
157
+ const inject = (ctx as unknown as { inject?: (keys: readonly string[], callback: (ctx: Context) => void) => unknown }).inject
158
+ if (typeof inject === 'function') {
159
+ inject.call(ctx, ['webServer'], webCtx => {
160
+ for (const legacyRoute of routes) {
161
+ // Same reason as above, on the other carrier: these are the `/data-sheet/...` URLs the
162
+ // browser console was reporting as bare 400s with nothing to go on.
163
+ const guarded: LegacyRoute = {
164
+ ...legacyRoute,
165
+ handler: async (req, res) => {
166
+ try {
167
+ await legacyRoute.handler(req, res)
168
+ } catch (error) {
169
+ const message = described(legacyRoute.path, error)
170
+ console.warn(`[data-sheet] ${message}`)
171
+ if (!res.headersSent) {
172
+ res.writeHead(500, { 'content-type': 'application/json' })
173
+ res.end(JSON.stringify({ error: message }))
174
+ } else {
175
+ res.end()
176
+ }
177
+ }
178
+ },
179
+ }
180
+ webCtx.effect(() => (webCtx as Context & { webServer: { register(route: LegacyRoute): () => unknown } }).webServer.register(guarded), `data-sheet: ${legacyRoute.path} web route`)
181
+ }
182
+ })
183
+ }
184
+ }
185
+
186
+ export async function readJsonBody(req: IncomingMessage): Promise<Record<string, unknown>> {
187
+ const chunks: Buffer[] = []
188
+ let size = 0
189
+ for await (const chunk of req) {
190
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))
191
+ size += buffer.length
192
+ // Snapshot limits are 25 MiB; leave a small envelope margin while still
193
+ // allowing a legitimate checkpoint to reach the Host without a browser
194
+ // side `Failed to fetch` caused by an undersized request parser.
195
+ if (size > 32 * 1024 * 1024) throw new DataSheetError('PAYLOAD_TOO_LARGE', 'request body too large', 413)
196
+ chunks.push(buffer)
197
+ }
198
+ const parsed: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8'))
199
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) throw new Error('body must be an object')
200
+ return parsed as Record<string, unknown>
201
+ }
202
+
203
+ /**
204
+ * Keep plugin routes behind the same browser-origin fence as the Host API.
205
+ * Loopback is allowed by default; non-loopback authorities must be explicitly
206
+ * listed in DSH_TRUSTED_HOSTS or match the local listening address.
207
+ */
208
+ export function guardDataSheetRequest(req: IncomingMessage, res: ServerResponse): boolean {
209
+ const hostHeader = typeof req.headers.host === 'string' ? req.headers.host.trim().toLowerCase() : ''
210
+ if (hostHeader.length === 0) {
211
+ res.writeHead(403, { 'content-type': 'application/json; charset=utf-8' })
212
+ res.end(JSON.stringify({ ok: false, error: '缺少 Host' }))
213
+ return false
214
+ }
215
+ let hostUrl: URL
216
+ try { hostUrl = new URL(`http://${hostHeader}`) } catch {
217
+ res.writeHead(403, { 'content-type': 'application/json; charset=utf-8' })
218
+ res.end(JSON.stringify({ ok: false, error: 'Host 无效' }))
219
+ return false
220
+ }
221
+ const hostname = hostUrl.hostname.toLowerCase()
222
+ const localAddress = (req.socket.localAddress ?? '').replace(/^::ffff:/, '').toLowerCase()
223
+ const loopback = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1'
224
+ || hostname === '[::1]'
225
+ const trusted = new Set((process.env.DSH_TRUSTED_HOSTS ?? '').split(',').map(item => item.trim().toLowerCase()).filter(Boolean))
226
+ const localMatch = localAddress.length > 0 && (hostname === localAddress || (localAddress === '::1' && hostname === 'localhost'))
227
+ if (!loopback && !localMatch && !trusted.has(hostHeader) && !trusted.has(hostname)) {
228
+ res.writeHead(403, { 'content-type': 'application/json; charset=utf-8' })
229
+ res.end(JSON.stringify({ ok: false, error: '请求 authority 不在信任范围内' }))
230
+ return false
231
+ }
232
+ const fetchSite = req.headers['sec-fetch-site']
233
+ if (fetchSite === 'cross-site') {
234
+ res.writeHead(403, { 'content-type': 'application/json; charset=utf-8' })
235
+ res.end(JSON.stringify({ ok: false, error: '拒绝跨站请求' }))
236
+ return false
237
+ }
238
+ const origin = typeof req.headers.origin === 'string' ? req.headers.origin : undefined
239
+ if (origin !== undefined) {
240
+ try {
241
+ if (new URL(origin).host.toLowerCase() !== hostUrl.host.toLowerCase()) throw new Error('origin mismatch')
242
+ } catch {
243
+ res.writeHead(403, { 'content-type': 'application/json; charset=utf-8' })
244
+ res.end(JSON.stringify({ ok: false, error: 'Origin 不匹配' }))
245
+ return false
246
+ }
247
+ }
248
+ return true
249
+ }
250
+
251
+ /** Resolve the per-tab session used to scope isolated worktrees. EventSource
252
+ * cannot send custom headers, so collaboration clients may provide `session`
253
+ * as a query parameter; normal fetch clients use the header. */
254
+ export function requireDataSheetSession(req: IncomingMessage, res: ServerResponse, url: URL): string | null {
255
+ const header = typeof req.headers['x-data-sheet-session'] === 'string' ? req.headers['x-data-sheet-session'] : ''
256
+ const query = url.searchParams.get('session') ?? ''
257
+ const value = (header || query).trim()
258
+ if (value.length === 0 || value.length > 256) {
259
+ writeOperationError(res, new DataSheetError('SESSION_SCOPE_DENIED', '缺少有效的 data-sheet session', 403))
260
+ return null
261
+ }
262
+ return value
263
+ }
264
+
265
+ export function writeOperationError(res: ServerResponse, error: unknown): void {
266
+ if (error instanceof WorkbookRevisionConflictError) {
267
+ res.writeHead(409, { 'content-type': 'application/json; charset=utf-8' })
268
+ res.end(JSON.stringify({
269
+ ok: false,
270
+ code: 'REVISION_CONFLICT',
271
+ error: error.message,
272
+ // Compatible legacy fields plus the unified cross-plugin body (PRD §5.1).
273
+ expectedRevision: error.expected,
274
+ revision: error.actual,
275
+ conflict: error.payload,
276
+ }))
277
+ return
278
+ }
279
+ const dataSheetError = error instanceof DataSheetError ? error : null
280
+ const status = dataSheetError?.status ?? 400
281
+ const code = dataSheetError?.code ?? 'VALIDATION_ERROR'
282
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
283
+ res.end(JSON.stringify({ ok: false, code, error: error instanceof Error ? error.message : String(error), ...(dataSheetError?.details ?? {}) }))
284
+ }
285
+
286
+ /**
287
+ * The `?after=` revision cursor.
288
+ *
289
+ * An absent (or blank) cursor means "from the beginning", which is a real value,
290
+ * not an error. Anything else has to parse — this used to swallow every failure
291
+ * into `0`, so `?after=abc` did not fail, it silently replayed the *entire*
292
+ * operation log at the asking client. A malformed cursor is now a 400 like any
293
+ * other bad parameter, which is what the surrounding handlers already do.
294
+ */
295
+ export function parseRevision(value: string | null): number {
296
+ const trimmed = value?.trim() ?? ''
297
+ if (trimmed.length === 0) return 0
298
+ // Decimal digits only. `Number()` would also accept `0x10` (16), `1e3` (1000)
299
+ // and `5.0` (5) — a cursor is a revision number, and accepting a spelling
300
+ // nobody writes is how a typo turns into a plausible-looking revision.
301
+ if (!/^\d+$/.test(trimmed)) throw new Error('after 必须是大于等于 0 的整数')
302
+ return parseIndex(Number(trimmed), 'after')
303
+ }
304
+
305
+ /**
306
+ * One matched `/data-sheet/workbook/...` path.
307
+ *
308
+ * Matching lives here, in the transport module, and dispatch lives in the domain
309
+ * modules: a handler can be read without working out which regexes it owns, and
310
+ * a new endpoint cannot silently shadow an existing one — the order of the `if`s
311
+ * inside a single 600-line handler used to decide that.
312
+ */
313
+ export type WorkbookRoute =
314
+ | { kind: 'view' | 'snapshot' | 'changes' | 'collaboration' | 'history' | 'history-diff' | 'range' | 'presence' | 'operation' | 'proposals' | 'rollback' | 'worktrees'; id: string }
315
+ | { kind: 'proposal-action'; id: string; proposalId: string; action: 'commit' | 'reject' }
316
+ | { kind: 'worktree' | 'worktree-operations' | 'worktree-diff'; id: string; worktreeId: string }
317
+ | { kind: 'worktree-action'; id: string; worktreeId: string; action: 'ready' | 'reopen' | 'merge' | 'discard' }
318
+
319
+ /**
320
+ * Matching for one workbook path.
321
+ *
322
+ * The id segment is `[^/]+` rather than an ASCII class: a workbook's id is now its
323
+ * file name (`yyyymmdd-中文名`, see `storageNameOf` in `#contracts`),
324
+ * and the browser percent-encodes it into the path. Spelling the character set out
325
+ * here would be a second copy of `storageSegment`'s rule — one that silently 404s a
326
+ * name the store happily created. What keeps a path inside the store is not this
327
+ * regex but the store itself: every path builder in `store/paths.ts` runs the id
328
+ * through `normalizeWorkbookId` before it becomes a file name.
329
+ */
330
+ const WORKBOOK_PATH = /^\/data-sheet\/workbook\/([^/]+?)(?:\.json)?(?:\/(.*))?$/
331
+ const PLAIN_ROUTES: Record<string, 'changes' | 'collaboration' | 'history' | 'history-diff' | 'range' | 'presence' | 'operation' | 'view' | 'proposals' | 'rollback' | 'worktrees'> = {
332
+ changes: 'changes', collaboration: 'collaboration', history: 'history',
333
+ 'history/diff': 'history-diff', range: 'range', presence: 'presence', operations: 'operation',
334
+ view: 'view', proposals: 'proposals', rollback: 'rollback', worktrees: 'worktrees',
335
+ }
336
+
337
+ /** Match one workbook path; null when nothing here claims it. */
338
+ export function matchWorkbookRoute(pathname: string): WorkbookRoute | null {
339
+ const match = WORKBOOK_PATH.exec(pathname)
340
+ if (match === null) return null
341
+ const id = match[1] as string
342
+ const rest = match[2]
343
+ if (rest === undefined) return { kind: 'snapshot', id }
344
+ const plain = PLAIN_ROUTES[rest]
345
+ if (plain !== undefined) return { kind: plain, id }
346
+ // Same `[^/]+` id segment as the workbook path above: worktree and proposal ids
347
+ // are minted from the workbook's, so they inherit its character set.
348
+ const proposalAction = /^proposals\/([^/]+)\/(commit|reject)$/.exec(rest)
349
+ if (proposalAction !== null) {
350
+ return { kind: 'proposal-action', id, proposalId: proposalAction[1] as string, action: proposalAction[2] as 'commit' | 'reject' }
351
+ }
352
+ const worktreeAction = /^worktrees\/([^/]+)\/(ready|reopen|merge|discard)$/.exec(rest)
353
+ if (worktreeAction !== null) {
354
+ return { kind: 'worktree-action', id, worktreeId: worktreeAction[1] as string, action: worktreeAction[2] as 'ready' | 'reopen' | 'merge' | 'discard' }
355
+ }
356
+ const worktreeChild = /^worktrees\/([^/]+)(?:\/(operations|diff))?$/.exec(rest)
357
+ if (worktreeChild !== null) {
358
+ const worktreeId = worktreeChild[1] as string
359
+ if (worktreeChild[2] === 'operations') return { kind: 'worktree-operations', id, worktreeId }
360
+ if (worktreeChild[2] === 'diff') return { kind: 'worktree-diff', id, worktreeId }
361
+ return { kind: 'worktree', id, worktreeId }
362
+ }
363
+ return null
364
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The standalone workbook viewer (docs/issue/0022 P3).
3
+ *
4
+ * One workbook, no shell: the panel can hand a workbook to a plain page, which
5
+ * is why this HTML is assembled on the server and served as `…/view`.
6
+ */
7
+ export function escapeHtml(value: string): string {
8
+ return value.replace(/[&<>'"]/g, character => ({
9
+ '&': '&amp;',
10
+ '<': '&lt;',
11
+ '>': '&gt;',
12
+ "'": '&#39;',
13
+ '"': '&quot;',
14
+ })[character] ?? character)
15
+ }
16
+
17
+ /** Minimal same-origin shell; the React page bundle owns the workbook UI. */
18
+ export function standaloneWorkbookHtml(title: string): string {
19
+ const safeTitle = escapeHtml(title)
20
+ return `<!doctype html>
21
+ <html lang="zh-CN">
22
+ <head>
23
+ <meta charset="utf-8">
24
+ <meta name="viewport" content="width=device-width, initial-scale=1">
25
+ <title>${safeTitle} - Table</title>
26
+ </head>
27
+ <body>
28
+ <div id="data-sheet-workbook-root"></div>
29
+ <script src="/data-sheet/workbook-page.js"></script>
30
+ </body>
31
+ </html>`
32
+ }