@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,206 @@
1
+ /**
2
+ * The project file chooser's data source.
3
+ *
4
+ * The browser's own file dialog cannot do the two things this plugin needs: it
5
+ * shows no creation date, and it can browse the whole disk, which is the wrong
6
+ * default for a project that keeps its data in `<project>/storages`. This route
7
+ * lists one directory of that container instead, with each entry's size and its
8
+ * creation and modification times.
9
+ *
10
+ * Two properties are deliberate:
11
+ *
12
+ * - **Contained.** `dir` is a relative path inside `<project>/storages`, and
13
+ * anything that resolves outside it is refused. The chooser is not a general
14
+ * filesystem browser.
15
+ * - **Not the only way in.** Importing from outside the project stays possible
16
+ * (the OS dialog the client already had): containment is about where a
17
+ * project's files *live*, not about what a user may read.
18
+ *
19
+ * @module dsh-data-table/http/project-files
20
+ */
21
+ import { readFile, readdir, stat } from 'node:fs/promises';
22
+ import { join, resolve, sep } from 'node:path';
23
+ import { projectFor, sessionIdOf } from "./project-scope.js";
24
+ import { guardDataSheetRequest } from "./routes.js";
25
+ import { projectStoragesDirectory, runInProject } from "../store/project.js";
26
+ /** Cap on one page; a chooser that renders ten thousand rows helps nobody. */
27
+ const MAX_ENTRIES = 500;
28
+ /**
29
+ * Normalize the requested directory to a relative path inside the storage root.
30
+ *
31
+ * @param raw - What the client sent: `''`, `data-table/`.
32
+ * @returns a `/`-separated relative path with no traversal.
33
+ * @throws when the path escapes the storage container.
34
+ */
35
+ export function safeRelativeDirectory(raw) {
36
+ // A leading separator or a `..` segment is refused rather than normalized
37
+ // away: `/etc` silently becoming `etc` would list a *different* directory than
38
+ // the caller asked for, and a caller that sends one is wrong about something.
39
+ const cleaned = raw.replace(/\\/g, '/').replace(/\/+$/, '');
40
+ if (cleaned.length === 0)
41
+ return '';
42
+ if (cleaned.startsWith('/'))
43
+ throw new Error('目录必须是 storages 内的相对路径');
44
+ const segments = cleaned.split('/');
45
+ if (segments.some(segment => segment === '' || segment === '.' || segment === '..')) {
46
+ throw new Error('目录必须是 storages 内的相对路径');
47
+ }
48
+ return segments.join('/');
49
+ }
50
+ /** ISO creation and modification times of one entry, best effort. */
51
+ function times(entry) {
52
+ // `birthtimeMs` is 0 on filesystems that do not record a creation time; an
53
+ // empty string is honest there, while `0` would render as 1970.
54
+ const created = Number.isFinite(entry.birthtimeMs) && entry.birthtimeMs > 0 ? new Date(entry.birthtimeMs) : null;
55
+ return {
56
+ createdAt: created === null ? '' : created.toISOString(),
57
+ modifiedAt: new Date(entry.mtimeMs).toISOString(),
58
+ };
59
+ }
60
+ /**
61
+ * List one page of a storage container rooted at `root`.
62
+ *
63
+ * `root` is the project directory, and the container is `<root>/storages`; the
64
+ * parameter exists so tests can point at a temporary tree without a Session.
65
+ */
66
+ export async function listProjectFiles(root, relativeDirectory) {
67
+ const directory = safeRelativeDirectory(relativeDirectory);
68
+ const target = join(projectStoragesDirectory(root), ...(directory === '' ? [] : directory.split('/')));
69
+ const containment = resolve(projectStoragesDirectory(root));
70
+ const resolved = resolve(target);
71
+ if (resolved !== containment && !resolved.startsWith(containment + sep)) {
72
+ throw new Error('目录必须是 storages 内的相对路径');
73
+ }
74
+ const dirents = await readdir(resolved, { withFileTypes: true }).catch(() => []);
75
+ const entries = [];
76
+ for (const dirent of dirents) {
77
+ const absolute = join(resolved, dirent.name);
78
+ const info = await stat(absolute).catch(() => null);
79
+ if (info === null)
80
+ continue;
81
+ const isDirectory = info.isDirectory();
82
+ if (!isDirectory && !info.isFile())
83
+ continue;
84
+ entries.push({
85
+ name: dirent.name,
86
+ path: directory === '' ? dirent.name : `${directory}/${dirent.name}`,
87
+ type: isDirectory ? 'directory' : 'file',
88
+ size: isDirectory ? 0 : info.size,
89
+ ...times(info),
90
+ });
91
+ }
92
+ entries.sort((left, right) => {
93
+ if (left.type !== right.type)
94
+ return left.type === 'directory' ? -1 : 1;
95
+ return left.name.localeCompare(right.name);
96
+ });
97
+ return { directory, truncated: Math.max(0, entries.length - MAX_ENTRIES), entries: entries.slice(0, MAX_ENTRIES) };
98
+ }
99
+ /** Largest file the chooser will hand back; matches the client's own import cap. */
100
+ export const MAX_PICKED_BYTES = 25 * 1024 * 1024;
101
+ /**
102
+ * Read one file from the storage container.
103
+ *
104
+ * The browser cannot open a path itself, so a choice made in the chooser comes
105
+ * back through here. Contained exactly like the listing — the same root, the
106
+ * same refusal — and capped, because the import pipeline caps at the same size
107
+ * and a truncated workbook is worse than a clear refusal.
108
+ *
109
+ * @param root - Project directory.
110
+ * @param relativePath - File path relative to `<project>/storages`.
111
+ * @param maxBytes - Cap, defaulting to {@link MAX_PICKED_BYTES}.
112
+ * @returns the file's bytes and its basename.
113
+ * @throws when the path escapes the container, is a directory, or is too large.
114
+ */
115
+ export async function readProjectFile(root, relativePath, maxBytes = MAX_PICKED_BYTES) {
116
+ const container = resolve(projectStoragesDirectory(root));
117
+ const cleaned = safeRelativeDirectory(relativePath);
118
+ if (cleaned === '')
119
+ throw new Error('请选择一个文件');
120
+ const resolved = resolve(join(container, ...cleaned.split('/')));
121
+ if (!resolved.startsWith(container + sep))
122
+ throw new Error('文件必须在 storages 内');
123
+ const info = await stat(resolved).catch(() => null);
124
+ if (info === null || !info.isFile())
125
+ throw new Error('文件不存在');
126
+ if (info.size > maxBytes)
127
+ throw new Error(`文件过大(最大 ${Math.floor(maxBytes / (1024 * 1024))} MB)`);
128
+ return { name: cleaned.split('/').pop() ?? cleaned, bytes: await readFile(resolved) };
129
+ }
130
+ /**
131
+ * The chooser's listing for one request.
132
+ *
133
+ * @param project - Absolute project directory, or `undefined` when none was named.
134
+ * @param relativeDirectory - Directory relative to the storage container.
135
+ * @returns the listing; empty when there is no project or no storage yet.
136
+ * @throws when the path escapes the container.
137
+ */
138
+ export async function projectFiles(project, relativeDirectory) {
139
+ if (project === undefined)
140
+ return { directory: safeRelativeDirectory(relativeDirectory), truncated: 0, entries: [] };
141
+ return runInProject(project, async () => listProjectFiles(project, relativeDirectory));
142
+ }
143
+ /**
144
+ * The chooser's two routes: the listing, and one file's bytes.
145
+ *
146
+ * They live here rather than in the plugin body because the body has a 200-line
147
+ * budget — it assembles routes, it does not implement them (the same reason
148
+ * `assetRoutes()` is a module).
149
+ *
150
+ * @param ctx - Host context, read for the Session registry.
151
+ * @returns the routes to register.
152
+ */
153
+ export function projectFileRoutes(ctx) {
154
+ /** The project this request belongs to, resolved once per request. */
155
+ const projectOf = (req, url) => projectFor(ctx, sessionIdOf(req.headers, url));
156
+ return [{
157
+ kind: 'exact',
158
+ path: '/data-sheet/project-files.json',
159
+ handler: async (req, res) => {
160
+ if (!guardDataSheetRequest(req, res))
161
+ return;
162
+ const url = new URL(req.url ?? '/', 'http://x');
163
+ try {
164
+ const listing = await projectFiles(projectOf(req, url), url.searchParams.get('dir') ?? '');
165
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' });
166
+ res.end(JSON.stringify({ ok: true, ...listing }));
167
+ }
168
+ catch (error) {
169
+ // A path outside the container is the caller's mistake, not a crash.
170
+ res.writeHead(400, { 'content-type': 'application/json; charset=utf-8' });
171
+ res.end(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }));
172
+ }
173
+ },
174
+ }, {
175
+ // One file's bytes, so a choice made in the chooser can reach the import
176
+ // pipeline: the browser cannot open a path by itself.
177
+ kind: 'exact',
178
+ path: '/data-sheet/project-file.json',
179
+ handler: async (req, res) => {
180
+ if (!guardDataSheetRequest(req, res))
181
+ return;
182
+ const url = new URL(req.url ?? '/', 'http://x');
183
+ const project = projectOf(req, url);
184
+ if (project === undefined) {
185
+ res.writeHead(400, { 'content-type': 'application/json; charset=utf-8' });
186
+ res.end(JSON.stringify({ ok: false, error: '无法确定项目目录' }));
187
+ return;
188
+ }
189
+ try {
190
+ const file = await readProjectFile(project, url.searchParams.get('path') ?? '');
191
+ res.writeHead(200, {
192
+ 'content-type': 'application/octet-stream',
193
+ 'content-length': String(file.bytes.byteLength),
194
+ // The chooser showed this name; the client keeps using it.
195
+ 'x-file-name': encodeURIComponent(file.name),
196
+ 'cache-control': 'no-cache',
197
+ });
198
+ res.end(file.bytes);
199
+ }
200
+ catch (error) {
201
+ res.writeHead(400, { 'content-type': 'application/json; charset=utf-8' });
202
+ res.end(JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) }));
203
+ }
204
+ },
205
+ }];
206
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Which project a browser request writes into.
3
+ *
4
+ * The workbook routes are declared in `index.ts` and dispatched to the domain
5
+ * handlers in `http/`, so the decision "this request belongs to that project"
6
+ * has to be made once, before the dispatch — but the plugin body has a 200-line
7
+ * budget precisely so that decisions with a home live in it. This is that home.
8
+ *
9
+ * @module dsh-data-table/http/project-scope
10
+ */
11
+ import type { Context } from '@deepseek-ai/cordis';
12
+ /**
13
+ * The Session a browser request belongs to.
14
+ *
15
+ * The header first, because that is one line in the client's single transport
16
+ * (`requestJson` sets it on every call); the query parameter second, because an
17
+ * `EventSource` cannot set headers and the live collaboration stream is the one
18
+ * route that needs the fallback.
19
+ *
20
+ * @param headers - Node request headers.
21
+ * @param url - Parsed request URL.
22
+ * @returns the Session id, or `undefined` for a caller that named none.
23
+ */
24
+ export declare function sessionIdOf(headers: Record<string, unknown>, url: URL): string | undefined;
25
+ /**
26
+ * The project directory one browser request writes into.
27
+ *
28
+ * A Session's header carries its working directory, and the registry is where
29
+ * that header is read — the same lookup the Agent tools make for the same
30
+ * Session id, so a panel and a tool call cannot mean two directories. `undefined`
31
+ * means **no project**: nobody named one, so the store keeps its pre-project
32
+ * location rather than inventing a project from the process directory — one Host
33
+ * serves several projects at once, and a shared fallback would mix them.
34
+ *
35
+ * @param ctx - Host context.
36
+ * @param sessionId - Session named by the request, when it named one.
37
+ * @returns an absolute project directory, or `undefined`.
38
+ */
39
+ export declare function projectFor(ctx: Context, sessionId: string | undefined): string | undefined;
40
+ /**
41
+ * Run one request's store work inside `project`, when there is one.
42
+ *
43
+ * A missing project is not a failure: the body runs unscoped and the store
44
+ * answers with the location it used before projects existed.
45
+ *
46
+ * @param project - Project directory, or `undefined` for "nobody named one".
47
+ * @param body - The request's store work.
48
+ * @returns whatever `body` returns.
49
+ */
50
+ export declare function runForProject<T>(project: string | undefined, body: () => Promise<T>): Promise<T>;
@@ -0,0 +1,52 @@
1
+ import { projectScopeOf, runInProject } from "../store/project.js";
2
+ /**
3
+ * The Session a browser request belongs to.
4
+ *
5
+ * The header first, because that is one line in the client's single transport
6
+ * (`requestJson` sets it on every call); the query parameter second, because an
7
+ * `EventSource` cannot set headers and the live collaboration stream is the one
8
+ * route that needs the fallback.
9
+ *
10
+ * @param headers - Node request headers.
11
+ * @param url - Parsed request URL.
12
+ * @returns the Session id, or `undefined` for a caller that named none.
13
+ */
14
+ export function sessionIdOf(headers, url) {
15
+ const header = headers['x-dsh-session'];
16
+ const value = Array.isArray(header) ? header[0] : header;
17
+ if (typeof value === 'string' && value.length > 0)
18
+ return value;
19
+ const query = url.searchParams.get('sessionId');
20
+ return query !== null && query.length > 0 ? query : undefined;
21
+ }
22
+ /**
23
+ * The project directory one browser request writes into.
24
+ *
25
+ * A Session's header carries its working directory, and the registry is where
26
+ * that header is read — the same lookup the Agent tools make for the same
27
+ * Session id, so a panel and a tool call cannot mean two directories. `undefined`
28
+ * means **no project**: nobody named one, so the store keeps its pre-project
29
+ * location rather than inventing a project from the process directory — one Host
30
+ * serves several projects at once, and a shared fallback would mix them.
31
+ *
32
+ * @param ctx - Host context.
33
+ * @param sessionId - Session named by the request, when it named one.
34
+ * @returns an absolute project directory, or `undefined`.
35
+ */
36
+ export function projectFor(ctx, sessionId) {
37
+ const scope = ctx;
38
+ return projectScopeOf(scope.sessions, sessionId);
39
+ }
40
+ /**
41
+ * Run one request's store work inside `project`, when there is one.
42
+ *
43
+ * A missing project is not a failure: the body runs unscoped and the store
44
+ * answers with the location it used before projects existed.
45
+ *
46
+ * @param project - Project directory, or `undefined` for "nobody named one".
47
+ * @param body - The request's store work.
48
+ * @returns whatever `body` returns.
49
+ */
50
+ export function runForProject(project, body) {
51
+ return project === undefined ? body() : runInProject(project, body);
52
+ }
@@ -0,0 +1,98 @@
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
+ /**
13
+ * Structural view of the host webserver route API (no manifest dependency).
14
+ *
15
+ * Declared here rather than in the plugin body because this is where the cast
16
+ * that needs it lives: `registerConnectionRoutes` registers the same handlers
17
+ * with both carriers.
18
+ */
19
+ interface WebServerLike {
20
+ register(route: {
21
+ kind: 'exact' | 'prefix';
22
+ path: string;
23
+ handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
24
+ }): () => void;
25
+ }
26
+ declare module '@deepseek-ai/cordis' {
27
+ interface Context {
28
+ webServer: WebServerLike;
29
+ }
30
+ }
31
+ export interface LegacyRoute {
32
+ readonly kind: 'exact' | 'prefix';
33
+ readonly path: string;
34
+ readonly handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
35
+ }
36
+ export interface LegacyResponse {
37
+ writeHead(status: number, headers?: Record<string, string>): void;
38
+ write(chunk: string | Uint8Array): boolean;
39
+ end(chunk?: string | Uint8Array): void;
40
+ once(event: 'close', listener: () => void): void;
41
+ on(event: 'drain', listener: () => void): void;
42
+ destroy(): void;
43
+ }
44
+ export declare function routeFor(routes: readonly LegacyRoute[], pathname: string): LegacyRoute | undefined;
45
+ /** Adapt the HTTP handlers to the official Electron shell's Connection carrier. */
46
+ export declare function dispatchConnectionRoute(routes: readonly LegacyRoute[], request: Request, prefix: string): Promise<Response>;
47
+ export declare function registerConnectionRoutes(ctx: Context, routes: readonly LegacyRoute[], prefix: string): void;
48
+ export declare function readJsonBody(req: IncomingMessage): Promise<Record<string, unknown>>;
49
+ /**
50
+ * Keep plugin routes behind the same browser-origin fence as the Host API.
51
+ * Loopback is allowed by default; non-loopback authorities must be explicitly
52
+ * listed in DSH_TRUSTED_HOSTS or match the local listening address.
53
+ */
54
+ export declare function guardDataSheetRequest(req: IncomingMessage, res: ServerResponse): boolean;
55
+ /** Resolve the per-tab session used to scope isolated worktrees. EventSource
56
+ * cannot send custom headers, so collaboration clients may provide `session`
57
+ * as a query parameter; normal fetch clients use the header. */
58
+ export declare function requireDataSheetSession(req: IncomingMessage, res: ServerResponse, url: URL): string | null;
59
+ export declare function writeOperationError(res: ServerResponse, error: unknown): void;
60
+ /**
61
+ * The `?after=` revision cursor.
62
+ *
63
+ * An absent (or blank) cursor means "from the beginning", which is a real value,
64
+ * not an error. Anything else has to parse — this used to swallow every failure
65
+ * into `0`, so `?after=abc` did not fail, it silently replayed the *entire*
66
+ * operation log at the asking client. A malformed cursor is now a 400 like any
67
+ * other bad parameter, which is what the surrounding handlers already do.
68
+ */
69
+ export declare function parseRevision(value: string | null): number;
70
+ /**
71
+ * One matched `/data-sheet/workbook/...` path.
72
+ *
73
+ * Matching lives here, in the transport module, and dispatch lives in the domain
74
+ * modules: a handler can be read without working out which regexes it owns, and
75
+ * a new endpoint cannot silently shadow an existing one — the order of the `if`s
76
+ * inside a single 600-line handler used to decide that.
77
+ */
78
+ export type WorkbookRoute = {
79
+ kind: 'view' | 'snapshot' | 'changes' | 'collaboration' | 'history' | 'history-diff' | 'range' | 'presence' | 'operation' | 'proposals' | 'rollback' | 'worktrees';
80
+ id: string;
81
+ } | {
82
+ kind: 'proposal-action';
83
+ id: string;
84
+ proposalId: string;
85
+ action: 'commit' | 'reject';
86
+ } | {
87
+ kind: 'worktree' | 'worktree-operations' | 'worktree-diff';
88
+ id: string;
89
+ worktreeId: string;
90
+ } | {
91
+ kind: 'worktree-action';
92
+ id: string;
93
+ worktreeId: string;
94
+ action: 'ready' | 'reopen' | 'merge' | 'discard';
95
+ };
96
+ /** Match one workbook path; null when nothing here claims it. */
97
+ export declare function matchWorkbookRoute(pathname: string): WorkbookRoute | null;
98
+ export {};