@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.
- package/LICENSE +21 -0
- package/README.md +172 -2
- package/cordis.patch.yml +7 -0
- package/lib/client.js +16766 -0
- package/lib/contract-envelope.d.ts +91 -0
- package/lib/contract-envelope.js +106 -0
- package/lib/contract-layout.d.ts +130 -0
- package/lib/contract-layout.js +138 -0
- package/lib/contract-refs.d.ts +39 -0
- package/lib/contract-refs.js +9 -0
- package/lib/contract-version.d.ts +311 -0
- package/lib/contract-version.js +270 -0
- package/lib/contract-wire.d.ts +147 -0
- package/lib/contract-wire.js +99 -0
- package/lib/contracts.d.ts +12 -0
- package/lib/contracts.js +12 -0
- package/lib/types/audit.d.ts +42 -0
- package/lib/types/audit.js +39 -0
- package/lib/types/client/ProjectImportMenu.d.ts +30 -0
- package/lib/types/client/ProjectImportMenu.js +70 -0
- package/lib/types/client/SheetPanel.d.ts +33 -0
- package/lib/types/client/SheetPanel.js +1766 -0
- package/lib/types/client/SheetToggle.d.ts +18 -0
- package/lib/types/client/SheetToggle.js +26 -0
- package/lib/types/client/index.d.ts +15 -0
- package/lib/types/client/index.js +110 -0
- package/lib/types/client/persistence/changes.d.ts +15 -0
- package/lib/types/client/persistence/changes.js +51 -0
- package/lib/types/client/persistence/ids.d.ts +34 -0
- package/lib/types/client/persistence/ids.js +48 -0
- package/lib/types/client/persistence/merge.d.ts +35 -0
- package/lib/types/client/persistence/merge.js +362 -0
- package/lib/types/client/store.d.ts +61 -0
- package/lib/types/client/store.js +152 -0
- package/lib/types/client/surface-context.d.ts +27 -0
- package/lib/types/client/surface-context.js +104 -0
- package/lib/types/client/surface-control.d.ts +2 -0
- package/lib/types/client/surface-control.js +12 -0
- package/lib/types/client/transport/workbookApi.d.ts +159 -0
- package/lib/types/client/transport/workbookApi.js +246 -0
- package/lib/types/client/univer/StandaloneWorkbookPage.d.ts +5 -0
- package/lib/types/client/univer/StandaloneWorkbookPage.js +7 -0
- package/lib/types/client/univer/WorkbookCanvas.d.ts +40 -0
- package/lib/types/client/univer/WorkbookCanvas.js +87 -0
- package/lib/types/client/univer/WorkbookView.d.ts +3 -0
- package/lib/types/client/univer/WorkbookView.js +2 -0
- package/lib/types/client/univer/loader.d.ts +18 -0
- package/lib/types/client/univer/loader.js +81 -0
- package/lib/types/client/univer/standalone-entry.d.ts +1 -0
- package/lib/types/client/univer/standalone-entry.js +19 -0
- package/lib/types/client/versions/VersionChrome.d.ts +33 -0
- package/lib/types/client/versions/VersionChrome.js +21 -0
- package/lib/types/client/versions/inspect.d.ts +70 -0
- package/lib/types/client/versions/inspect.js +88 -0
- package/lib/types/client/versions/preview-decoration.d.ts +11 -0
- package/lib/types/client/versions/preview-decoration.js +54 -0
- package/lib/types/client/versions/records.d.ts +19 -0
- package/lib/types/client/versions/records.js +70 -0
- package/lib/types/client/workspace-host.d.ts +61 -0
- package/lib/types/client/workspace-host.js +124 -0
- package/lib/types/collaboration-hub.d.ts +31 -0
- package/lib/types/collaboration-hub.js +69 -0
- package/lib/types/contract/ids.d.ts +24 -0
- package/lib/types/contract/ids.js +46 -0
- package/lib/types/contract/mutations.d.ts +25 -0
- package/lib/types/contract/mutations.js +782 -0
- package/lib/types/contract/operations.d.ts +54 -0
- package/lib/types/contract/operations.js +203 -0
- package/lib/types/contract/snapshots.d.ts +176 -0
- package/lib/types/contract/snapshots.js +351 -0
- package/lib/types/contract/univer-api-catalog.d.ts +29 -0
- package/lib/types/contract/univer-api-catalog.js +264 -0
- package/lib/types/errors.d.ts +8 -0
- package/lib/types/errors.js +12 -0
- package/lib/types/http/assets-routes.d.ts +3 -0
- package/lib/types/http/assets-routes.js +169 -0
- package/lib/types/http/collaboration.d.ts +10 -0
- package/lib/types/http/collaboration.js +154 -0
- package/lib/types/http/history.d.ts +15 -0
- package/lib/types/http/history.js +134 -0
- package/lib/types/http/project-files.d.ts +79 -0
- package/lib/types/http/project-files.js +206 -0
- package/lib/types/http/project-scope.d.ts +50 -0
- package/lib/types/http/project-scope.js +52 -0
- package/lib/types/http/routes.d.ts +98 -0
- package/lib/types/http/routes.js +323 -0
- package/lib/types/http/standalone.d.ts +9 -0
- package/lib/types/http/standalone.js +31 -0
- package/lib/types/http/trunk.d.ts +10 -0
- package/lib/types/http/trunk.js +322 -0
- package/lib/types/http/worktree.d.ts +9 -0
- package/lib/types/http/worktree.js +145 -0
- package/lib/types/index.d.ts +18 -0
- package/lib/types/index.js +156 -0
- package/lib/types/invariant.d.ts +15 -0
- package/lib/types/invariant.js +15 -0
- package/lib/types/request-params.d.ts +21 -0
- package/lib/types/request-params.js +76 -0
- package/lib/types/store/paths.d.ts +68 -0
- package/lib/types/store/paths.js +148 -0
- package/lib/types/store/project.d.ts +88 -0
- package/lib/types/store/project.js +120 -0
- package/lib/types/store/read.d.ts +110 -0
- package/lib/types/store/read.js +404 -0
- package/lib/types/store/retention.d.ts +27 -0
- package/lib/types/store/retention.js +111 -0
- package/lib/types/store/write.d.ts +22 -0
- package/lib/types/store/write.js +210 -0
- package/lib/types/tools/arguments.d.ts +40 -0
- package/lib/types/tools/arguments.js +130 -0
- package/lib/types/tools/cells.d.ts +50 -0
- package/lib/types/tools/cells.js +298 -0
- package/lib/types/tools/index.d.ts +2 -0
- package/lib/types/tools/index.js +250 -0
- package/lib/types/tools/render.d.ts +47 -0
- package/lib/types/tools/render.js +290 -0
- package/lib/types/tools/univer-api.d.ts +18 -0
- package/lib/types/tools/univer-api.js +198 -0
- package/lib/types/tools/workbook-catalog.d.ts +3 -0
- package/lib/types/tools/workbook-catalog.js +83 -0
- package/lib/types/tools/workbook-edit.d.ts +192 -0
- package/lib/types/tools/workbook-edit.js +912 -0
- package/lib/types/tools/workbook-find.d.ts +11 -0
- package/lib/types/tools/workbook-find.js +89 -0
- package/lib/types/tools/workbook-history.d.ts +2 -0
- package/lib/types/tools/workbook-history.js +59 -0
- package/lib/types/tools/workbook-read.d.ts +21 -0
- package/lib/types/tools/workbook-read.js +204 -0
- package/lib/types/types.d.ts +377 -0
- package/lib/types/types.js +34 -0
- package/lib/types/univer/apply-operation.d.ts +14 -0
- package/lib/types/univer/apply-operation.js +398 -0
- package/lib/types/univer/chunk.d.ts +68 -0
- package/lib/types/univer/chunk.js +16 -0
- package/lib/types/univer/decoration.d.ts +34 -0
- package/lib/types/univer/decoration.js +88 -0
- package/lib/types/univer/diagnostics.d.ts +31 -0
- package/lib/types/univer/diagnostics.js +141 -0
- package/lib/types/univer/engine.d.ts +33 -0
- package/lib/types/univer/engine.js +338 -0
- package/lib/types/univer/entry.d.ts +8 -0
- package/lib/types/univer/entry.js +11 -0
- package/lib/types/univer/facade.d.ts +107 -0
- package/lib/types/univer/facade.js +1 -0
- package/lib/types/univer/fonts.d.ts +202 -0
- package/lib/types/univer/fonts.js +140 -0
- package/lib/types/univer/plugins/collaboration.d.ts +23 -0
- package/lib/types/univer/plugins/collaboration.js +47 -0
- package/lib/types/univer/plugins/export.d.ts +39 -0
- package/lib/types/univer/plugins/export.js +246 -0
- package/lib/types/univer/plugins/history.d.ts +24 -0
- package/lib/types/univer/plugins/history.js +50 -0
- package/lib/types/univer/plugins/version.d.ts +13 -0
- package/lib/types/univer/plugins/version.js +13 -0
- package/lib/types/version/diff.d.ts +36 -0
- package/lib/types/version/diff.js +156 -0
- package/lib/types/version/grouping.d.ts +7 -0
- package/lib/types/version/grouping.js +94 -0
- package/lib/types/version/records.d.ts +17 -0
- package/lib/types/version/records.js +102 -0
- package/lib/types/version/session.d.ts +36 -0
- package/lib/types/version/session.js +89 -0
- package/lib/types/version/summary.d.ts +24 -0
- package/lib/types/version/summary.js +111 -0
- package/lib/types/workbook-manager.d.ts +191 -0
- package/lib/types/workbook-manager.js +935 -0
- package/lib/types/xlsx-export-worker.d.ts +5 -0
- package/lib/types/xlsx-export-worker.js +24 -0
- package/lib/types/xlsx-export.d.ts +21 -0
- package/lib/types/xlsx-export.js +371 -0
- package/lib/types/xlsx-import-worker.d.ts +1 -0
- package/lib/types/xlsx-import-worker.js +19 -0
- package/lib/univer-client.js +690 -0
- package/lib/workbook-page.js +23826 -0
- package/lib/xlsx-export-worker.js +104 -0
- package/lib/xlsx-import-worker.js +46 -0
- package/package.json +106 -3
- package/src/audit.ts +58 -0
- package/src/client/ProjectImportMenu.tsx +100 -0
- package/src/client/SheetPanel.module.css +1578 -0
- package/src/client/SheetPanel.tsx +1934 -0
- package/src/client/SheetToggle.module.css +73 -0
- package/src/client/SheetToggle.tsx +65 -0
- package/src/client/contract.d.ts +19 -0
- package/src/client/index.ts +131 -0
- package/src/client/persistence/changes.ts +58 -0
- package/src/client/persistence/ids.ts +60 -0
- package/src/client/persistence/merge.ts +392 -0
- package/src/client/store.ts +188 -0
- package/src/client/surface-context.ts +123 -0
- package/src/client/surface-control.ts +13 -0
- package/src/client/transport/workbookApi.ts +355 -0
- package/src/client/univer/SheetView.module.css +110 -0
- package/src/client/univer/StandaloneWorkbookPage.module.css +8 -0
- package/src/client/univer/StandaloneWorkbookPage.tsx +15 -0
- package/src/client/univer/WorkbookCanvas.tsx +128 -0
- package/src/client/univer/WorkbookView.module.css +30 -0
- package/src/client/univer/WorkbookView.tsx +3 -0
- package/src/client/univer/loader.ts +90 -0
- package/src/client/univer/react-dom-client.d.ts +10 -0
- package/src/client/univer/standalone-entry.tsx +19 -0
- package/src/client/univer/standalone.css +12 -0
- package/src/client/univer/univer-locales.d.ts +89 -0
- package/src/client/univer/univer-modules.d.ts +26 -0
- package/src/client/versions/VersionChrome.tsx +114 -0
- package/src/client/versions/inspect.ts +138 -0
- package/src/client/versions/preview-decoration.ts +54 -0
- package/src/client/versions/records.ts +78 -0
- package/src/client/workspace-host.ts +147 -0
- package/src/collaboration-hub.ts +83 -0
- package/src/contract/ids.ts +50 -0
- package/src/contract/mutations.ts +791 -0
- package/src/contract/operations.ts +221 -0
- package/src/contract/snapshots.ts +383 -0
- package/src/contract/univer-api-catalog.ts +289 -0
- package/src/css-modules.d.ts +6 -0
- package/src/errors.ts +25 -0
- package/src/http/assets-routes.ts +177 -0
- package/src/http/collaboration.ts +144 -0
- package/src/http/history.ts +151 -0
- package/src/http/project-files.ts +227 -0
- package/src/http/project-scope.ts +77 -0
- package/src/http/routes.ts +364 -0
- package/src/http/standalone.ts +32 -0
- package/src/http/trunk.ts +322 -0
- package/src/http/worktree.ts +119 -0
- package/src/index.ts +165 -0
- package/src/invariant.ts +17 -0
- package/src/request-params.ts +92 -0
- package/src/store/paths.ts +165 -0
- package/src/store/project.ts +137 -0
- package/src/store/read.ts +454 -0
- package/src/store/retention.ts +106 -0
- package/src/store/write.ts +230 -0
- package/src/tools/arguments.ts +128 -0
- package/src/tools/cells.ts +298 -0
- package/src/tools/index.ts +274 -0
- package/src/tools/render.ts +301 -0
- package/src/tools/univer-api.ts +243 -0
- package/src/tools/workbook-catalog.ts +94 -0
- package/src/tools/workbook-edit.ts +987 -0
- package/src/tools/workbook-find.ts +80 -0
- package/src/tools/workbook-history.ts +63 -0
- package/src/tools/workbook-read.ts +210 -0
- package/src/types.ts +432 -0
- package/src/univer/apply-operation.ts +369 -0
- package/src/univer/chunk.ts +77 -0
- package/src/univer/decoration.ts +111 -0
- package/src/univer/diagnostics.ts +136 -0
- package/src/univer/engine.ts +370 -0
- package/src/univer/entry.ts +14 -0
- package/src/univer/facade.ts +91 -0
- package/src/univer/fonts.ts +148 -0
- package/src/univer/overrides.css +33 -0
- package/src/univer/plugins/collaboration.ts +47 -0
- package/src/univer/plugins/export.ts +236 -0
- package/src/univer/plugins/history.ts +49 -0
- package/src/univer/plugins/version.ts +13 -0
- package/src/version/diff.ts +169 -0
- package/src/version/grouping.ts +96 -0
- package/src/version/records.ts +113 -0
- package/src/version/session.ts +90 -0
- package/src/version/summary.ts +117 -0
- package/src/workbook-manager.ts +1111 -0
- package/src/xlsx-export-worker.ts +34 -0
- package/src/xlsx-export.ts +373 -0
- 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
|
+
'&': '&',
|
|
10
|
+
'<': '<',
|
|
11
|
+
'>': '>',
|
|
12
|
+
"'": ''',
|
|
13
|
+
'"': '"',
|
|
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
|
+
}
|