@deepblend/dsh-blender-ui 0.1.0 → 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/lib/client.js +1226 -725
- package/lib/index.js +144 -0
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -46,6 +46,95 @@ import {
|
|
|
46
46
|
/** Route path serving the settings card document (M0's surface, kept verbatim). */
|
|
47
47
|
export const CAPABILITIES_ROUTE = '/deepblend/capabilities'
|
|
48
48
|
|
|
49
|
+
/** Route path serving the standalone fullscreen workbench document (SPEC §20 M6). */
|
|
50
|
+
export const WORKBENCH_PAGE_ROUTE = '/deepblend/workbench'
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The element the standalone page reserves for the workbench.
|
|
54
|
+
*
|
|
55
|
+
* Named once, and read by the page's own bootstrap below and by
|
|
56
|
+
* `e2e/workbench-page.e2e.mjs`, so "the page mounted something" is answered by
|
|
57
|
+
* the same string on both sides.
|
|
58
|
+
*/
|
|
59
|
+
export const WORKBENCH_PAGE_ROOT_ID = 'deepblend-workbench'
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The bootstrap the standalone page runs.
|
|
63
|
+
*
|
|
64
|
+
* Twelve lines, and every one of them is about ARRIVING at the bundle rather
|
|
65
|
+
* than about the workbench:
|
|
66
|
+
*
|
|
67
|
+
* 1. `__DSH_BOOT_READY__` — the tail injection, resolved once the boot rows
|
|
68
|
+
* have run. Awaiting it is what makes this script safe wherever the
|
|
69
|
+
* injection table happens to be spliced.
|
|
70
|
+
* 2. `create({ boot: __DSH_BOOT__, staticModules: {} })` — the SAME module
|
|
71
|
+
* system the console builds, over the SAME graph global. The seed is empty
|
|
72
|
+
* because this page supplies no platform modules: React is a console seed
|
|
73
|
+
* word, the workbench core does not use it, and `client.js` reads it lazily
|
|
74
|
+
* precisely so that this call can pass `{}`.
|
|
75
|
+
* 3. `import('@deepblend/dsh-blender-ui')` — the console's own graph row,
|
|
76
|
+
* fetched from the console's own bundle route. There is no second copy of
|
|
77
|
+
* the workbench anywhere for this to find.
|
|
78
|
+
* 4. `mountStandalone(root)` — the one function that differs between the faces.
|
|
79
|
+
*
|
|
80
|
+
* A failure is reported INTO the page rather than only to the console, because a
|
|
81
|
+
* blank full-screen page is indistinguishable from a page that never booted.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} rootId
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
export function workbenchPageBootstrap(rootId) {
|
|
87
|
+
return `<script>(async () => {
|
|
88
|
+
const root = document.getElementById(${JSON.stringify(rootId)})
|
|
89
|
+
try {
|
|
90
|
+
await globalThis.__DSH_BOOT_READY__?.promise
|
|
91
|
+
const modules = window.__ModuleLoader__.create({ boot: window.__DSH_BOOT__, staticModules: {} })
|
|
92
|
+
const bundle = await modules.import('@deepblend/dsh-blender-ui')
|
|
93
|
+
await bundle.mountStandalone(root)
|
|
94
|
+
root.dataset.deepblendStandalone = 'ready'
|
|
95
|
+
} catch (error) {
|
|
96
|
+
root.dataset.deepblendStandalone = 'failed'
|
|
97
|
+
root.textContent = 'DeepBlend 工作台未能启动:' + String((error && error.message) || error)
|
|
98
|
+
}
|
|
99
|
+
})()</script>`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The standalone workbench document.
|
|
104
|
+
*
|
|
105
|
+
* Deliberately minimal, and deliberately NOT the console: no shell bundle, no
|
|
106
|
+
* sidebar, no conversation — one root element and the bootstrap. Everything the
|
|
107
|
+
* page shows arrives from the bundle the bootstrap imports and from the Host
|
|
108
|
+
* routes that bundle reads.
|
|
109
|
+
*
|
|
110
|
+
* `<head>` and `<body>` are present because `webServer.renderIndex` splices the
|
|
111
|
+
* boot injections into them; without the tags the rows would be prepended to the
|
|
112
|
+
* document instead, and the bootstrap script would run before the module loader
|
|
113
|
+
* existed.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} rootId
|
|
116
|
+
* @param {{ title?: string }} [options]
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
export function workbenchPageDocument(rootId, options = {}) {
|
|
120
|
+
const title = options.title ?? 'DeepBlend 工作台'
|
|
121
|
+
return [
|
|
122
|
+
'<!doctype html>',
|
|
123
|
+
'<html lang="zh">',
|
|
124
|
+
'<head>',
|
|
125
|
+
'<meta charset="utf-8">',
|
|
126
|
+
'<meta name="viewport" content="width=device-width,initial-scale=1">',
|
|
127
|
+
`<title>${title}</title>`,
|
|
128
|
+
'</head>',
|
|
129
|
+
`<body style="margin:0;height:100vh">`,
|
|
130
|
+
`<div id="${rootId}" data-deepblend-standalone="loading" style="height:100vh"></div>`,
|
|
131
|
+
workbenchPageBootstrap(rootId),
|
|
132
|
+
'</body>',
|
|
133
|
+
'</html>',
|
|
134
|
+
'',
|
|
135
|
+
].join('\n')
|
|
136
|
+
}
|
|
137
|
+
|
|
49
138
|
/** Service key for the UI-facing host controller. */
|
|
50
139
|
export const BLENDER_UI_SERVICE = 'blenderUi'
|
|
51
140
|
|
|
@@ -96,6 +185,12 @@ export default class BlenderUiHost extends Service {
|
|
|
96
185
|
assertKnownConfigKeys(UiConfig, config, 'deepblend-blender-ui')
|
|
97
186
|
this.config = config
|
|
98
187
|
this._routeDisposer = null
|
|
188
|
+
// Kept for ONE reason: the standalone page document has to be rendered
|
|
189
|
+
// through `webServer.renderIndex`, which is what splices the boot injections
|
|
190
|
+
// (`window.__ModuleLoader__`, `window.__DSH_BOOT__`, the bootstrap batch) into
|
|
191
|
+
// a body. That is the whole of this row's dependence on the server object —
|
|
192
|
+
// no file, no process, no path.
|
|
193
|
+
this._webServer = null
|
|
99
194
|
|
|
100
195
|
// One handler per route id, and no other way in. A test asserts the key set
|
|
101
196
|
// equals UI_ROUTES exactly, in both directions.
|
|
@@ -104,6 +199,7 @@ export default class BlenderUiHost extends Service {
|
|
|
104
199
|
if (!config.serveRoute) return
|
|
105
200
|
|
|
106
201
|
ctx.inject(['webServer'], (serverCtx) => {
|
|
202
|
+
this._webServer = serverCtx.webServer
|
|
107
203
|
// Route registration belongs to this fiber, so unmounting the row removes
|
|
108
204
|
// the route (SPEC §4.4 / lifecycle: every side effect is reversible).
|
|
109
205
|
const dispose = serverCtx.webServer.register({
|
|
@@ -114,6 +210,7 @@ export default class BlenderUiHost extends Service {
|
|
|
114
210
|
this._routeDisposer = dispose
|
|
115
211
|
return () => {
|
|
116
212
|
this._routeDisposer = null
|
|
213
|
+
this._webServer = null
|
|
117
214
|
dispose()
|
|
118
215
|
}
|
|
119
216
|
})
|
|
@@ -156,6 +253,13 @@ export default class BlenderUiHost extends Service {
|
|
|
156
253
|
await this._sendArtifact(request, response, matched.params)
|
|
157
254
|
return
|
|
158
255
|
}
|
|
256
|
+
// The second response that is not JSON, and the only one that is a
|
|
257
|
+
// document: the standalone workbench. It reads nothing from the request —
|
|
258
|
+
// no query, no body, no parameter — so there is no input here to validate.
|
|
259
|
+
if (matched.route.id === 'workbench.page') {
|
|
260
|
+
this._sendWorkbenchPage(request, response)
|
|
261
|
+
return
|
|
262
|
+
}
|
|
159
263
|
const body = await readRequestBody(request)
|
|
160
264
|
const result = await this._handlers[matched.route.id]({
|
|
161
265
|
params: matched.params,
|
|
@@ -201,6 +305,46 @@ export default class BlenderUiHost extends Service {
|
|
|
201
305
|
response.end(request.method === 'HEAD' ? undefined : artifact.bytes)
|
|
202
306
|
}
|
|
203
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Serve the standalone fullscreen workbench document (SPEC §20 M6).
|
|
310
|
+
*
|
|
311
|
+
* This is the only route that answers with HTML, and the only place in this
|
|
312
|
+
* package that asks the web server for anything other than a route seat. It
|
|
313
|
+
* reads no request input, touches no project and no path, and derives nothing
|
|
314
|
+
* from the caller: the document is a constant plus whatever the composition's
|
|
315
|
+
* own index injections add.
|
|
316
|
+
*
|
|
317
|
+
* `renderIndex` is what makes the page a real client surface rather than a
|
|
318
|
+
* static one — it splices in the boot rows (`window.__ModuleLoader__`, the
|
|
319
|
+
* `@deepseek-ai/dsh-client-modules` bootstrap, `window.__DSH_BOOT__`) that the
|
|
320
|
+
* console's own index.html gets, so the page can import this package's client
|
|
321
|
+
* bundle out of the graph instead of being handed a copy of it.
|
|
322
|
+
*
|
|
323
|
+
* When there is no web server there is no route either (the row only registers
|
|
324
|
+
* one inside `ctx.inject(['webServer'])`), so the fallback below is unreachable
|
|
325
|
+
* in a composed deployment; it exists so that driving `_handle` against a stub
|
|
326
|
+
* server in a test cannot throw on a missing method.
|
|
327
|
+
*
|
|
328
|
+
* @param {import('node:http').IncomingMessage} request
|
|
329
|
+
* @param {import('node:http').ServerResponse} response
|
|
330
|
+
*/
|
|
331
|
+
_sendWorkbenchPage(request, response) {
|
|
332
|
+
const document = workbenchPageDocument(WORKBENCH_PAGE_ROOT_ID)
|
|
333
|
+
const server = this._webServer
|
|
334
|
+
const html = server !== null && typeof server.renderIndex === 'function'
|
|
335
|
+
? server.renderIndex(document)
|
|
336
|
+
: document
|
|
337
|
+
const bytes = Buffer.from(html, 'utf8')
|
|
338
|
+
response.setHeader('content-type', 'text/html; charset=utf-8')
|
|
339
|
+
// Never cached, for the same reason the JSON routes are not: this document
|
|
340
|
+
// names the bundle revision it will load, and a cached copy would pin a page
|
|
341
|
+
// to a revision the process no longer serves.
|
|
342
|
+
response.setHeader('cache-control', 'no-store')
|
|
343
|
+
response.setHeader('content-length', String(bytes.length))
|
|
344
|
+
response.statusCode = 200
|
|
345
|
+
response.end(request.method === 'HEAD' ? undefined : bytes)
|
|
346
|
+
}
|
|
347
|
+
|
|
204
348
|
/**
|
|
205
349
|
* @param {import('node:http').ServerResponse} response
|
|
206
350
|
* @param {number} status
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepblend/dsh-blender-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DeepBlend workbench UI: Host half (typed HTTP API) and Web Client half (sidebar, panels, tool cards).",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@deepblend/dsh-blender-contracts": "0.
|
|
22
|
+
"@deepblend/dsh-blender-contracts": "0.2.0"
|
|
23
23
|
},
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"repository": {
|