@deepseek-ai/dsh-host-webserver 0.1.1-rc.2 → 0.1.2-alpha.2
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/README.i18n.yaml +2 -2
- package/README.md +106 -5
- package/README.zh.md +107 -6
- package/lib/index.js +70 -18
- package/lib/types/index.d.ts +13 -8
- package/lib/types/injections.d.ts +6 -1
- package/package.json +10 -6
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/host/webserver/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 4211c640a0e7386e3380a4289e5ba619818bd094
|
|
6
|
+
README.zh.md: 6ad4f549cd234221cfef88ef0bfd9541fbe50043
|
package/README.md
CHANGED
|
@@ -1,16 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "The web GUI host's HTTP server: named-route and upgrade registration, index transforms, and the single fallback seat that serves the Web shell's SPA dist."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-host-webserver
|
|
2
7
|
|
|
3
8
|
English | [中文](README.zh.md)
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
Browsers reach the web GUI over HTTP through `dsh-host-webserver`: a `node:http` server where other plugins register named routes, upgrade routes, index startup inputs, and one fallback handler. It knows no harness concepts and serves no files — the `/api` bridge, plugin bundles, the HMR event stream, and the SPA dist belong to the plugins that register them. Route matching is fixed: exact over the whole table, then longest prefix, then the fallback handler. It serves browsers only; Electron loads dist over `file://` and carries fetch over an IPC bridge.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
28
|
+
Compose the webserver as the HTTP transport of a browser-facing host, then let the feature plugins claim their routes. Activation listens immediately; registration order carries no request-facing semantics because named routes compose to be disjoint.
|
|
29
|
+
|
|
30
|
+
### Minimal configuration
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
- name: '@deepseek-ai/dsh-host-webserver'
|
|
34
|
+
config:
|
|
35
|
+
host: 127.0.0.1
|
|
36
|
+
port: 3000
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`host` accepts exactly two values: `127.0.0.1` (default posture, loopback only) and `0.0.0.0` (deliberate network exposure — the server carries no TLS, authentication, or origin policy of its own). `port` 0 requests an OS-assigned port; `ctx.webServer.port` reads the listening port afterwards.
|
|
40
|
+
|
|
41
|
+
Set `compression: 'gzip'` to wrap eligible socket-backed responses without changing route APIs. The client must accept gzip and the media type must be compressible; known response lengths below `compressionThresholdBytes` remain uncompressed, while unknown-length streams are eligible immediately. Existing encodings, `Cache-Control: no-transform`, range responses, SSE, ZIP, and the packaged `.gz` Worker image remain unchanged. The shipped Web bundle uses compression level 1 with a 1024-byte threshold; other compositions default to no compression.
|
|
42
|
+
|
|
43
|
+
### Registering routes
|
|
44
|
+
|
|
45
|
+
`register(route)` adds a named `exact` or `prefix` HTTP route, `registerUpgrade(route)` adds an upgrade route for an exact pathname, and both return a disposer that removes the registration. A duplicate path within either table throws — route patterns are a composition-level contract, so a collision is a misconfiguration. HTTP matching is exact over the whole table, then longest prefix, then the fallback handler; upgrades match exactly and unmatched connections are closed.
|
|
46
|
+
|
|
47
|
+
### The fallback seat
|
|
48
|
+
|
|
49
|
+
`registerFallback(handler)` claims the one handler for every request no named route matches. A second registration throws; while no fallback is registered the server answers 404. In the shipped Web composition the [SPA dist server](../frontend-static/README.md) owns the seat and calls `renderIndex` on every index response it renders.
|
|
50
|
+
|
|
51
|
+
Index startup inputs are two layers. `collectIndexInjections()` gathers a fresh injection table — one `webserver/index-inject` emit per call, each subscriber pushing its current rows — and `renderIndex(html)` renders those rows into the index.html body before applying the raw `tapIndex(transform)` transforms in registration order. A `script-preload` row renders an advisory classic-script preload link. Static deployments carry the same rows in their boot payload. `applyIndexTaps(html)` applies only the raw transforms; it is the escape hatch for markup no row expresses.
|
|
52
|
+
|
|
53
|
+
### Behavior under failure
|
|
54
|
+
|
|
55
|
+
A listen failure (for example EADDRINUSE) rejects plugin initialization with the bind diagnostic. An HTTP request whose handler throws is answered 400 — or the socket destroyed when headers are already out — and logged as a warning; it never exits the process. An upgrade-handler exception or upgraded-socket transport error logs a warning and destroys its socket.
|
|
56
|
+
|
|
57
|
+
-----
|
|
6
58
|
|
|
7
|
-
|
|
59
|
+
<a id="understand-the-implementation"></a>
|
|
60
|
+
## Understand the implementation
|
|
8
61
|
|
|
9
|
-
|
|
62
|
+
<details>
|
|
63
|
+
<summary>Implementation internals — click to expand</summary>
|
|
10
64
|
|
|
65
|
+
### Design concept
|
|
66
|
+
|
|
67
|
+
The package is a plain route registry with no harness vocabulary: `WebServer` extends Cordis `Service` and holds three route tables plus the fallback slot, the raw index-tap list, and the `webserver/index-inject` event the index renderer gathers rows through. Index rendering composes two layers per response: `renderIndex` renders the fresh injection table, including advisory `script-preload` rows, into the body, then applies the raw taps in registration order; `applyIndexTaps` runs the taps alone. The upgrade handler owns the protocol handshake and connection contents; the webserver only delivers the raw socket and request. `host` and `port` getters expose composition-time facts other plugins adapt to (for example the directory-picker chooser).
|
|
68
|
+
|
|
69
|
+
### Matching and lifecycle
|
|
70
|
+
|
|
71
|
+
`match(pathname)` consults the exact table first, then walks the prefix table for the longest match, then the fallback. Activation (`[Service.init]`) listens immediately; disposal starts `close()` and `closeAllConnections()`, destroys every tracked upgraded socket, and returns only after the server and those sockets have closed. Node does not include upgraded sockets in `closeAllConnections()`, so the service tracks them explicitly.
|
|
72
|
+
|
|
73
|
+
### Source map
|
|
74
|
+
|
|
75
|
+
| File | Role |
|
|
76
|
+
|---|---|
|
|
77
|
+
| [`src/index.ts`](src/index.ts) | `WebServer` service: route tables, fallback seat, index rendering, matching, lifecycle |
|
|
78
|
+
| [`src/injections.ts`](src/injections.ts) | Structured `IndexInjection` rows and `renderIndexInjections` row rendering |
|
|
79
|
+
|
|
80
|
+
</details>
|
|
81
|
+
|
|
82
|
+
-----
|
|
83
|
+
|
|
84
|
+
<a id="further-exploration"></a>
|
|
85
|
+
## Further Exploration
|
|
86
|
+
|
|
87
|
+
Read these when the server contract is not enough: the subsystem reference, then the fallback owner and the layering decision behind who registers which route.
|
|
88
|
+
|
|
89
|
+
- [HTTP server subsystem](../../../docs/subsystems/web-server.md) — routes, matching order, and the config the server accepts.
|
|
90
|
+
- [SPA dist server](../frontend-static/README.md) — the shipped owner of the fallback seat.
|
|
91
|
+
- [Web config-tree boot and transport layering](../../../.agents/notes/implemented/architecture/2026-07-24-web-config-tree-boot-and-transport-layering.md) — why feature plugins own every route.
|
|
92
|
+
- [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-host-webserver) — every accepted config field and its source declaration.
|
|
93
|
+
|
|
94
|
+
-----
|
|
95
|
+
|
|
96
|
+
<a id="model-experience"></a>
|
|
11
97
|
## Model Experience
|
|
12
98
|
|
|
13
|
-
None, as the
|
|
99
|
+
None, as the HTTP carrier bridges browser and API handler and registers nothing model-facing.
|
|
14
100
|
|
|
15
101
|
#### KV Cache effect
|
|
16
102
|
|
|
@@ -18,5 +104,20 @@ None; this package neither assembles nor sends a provider request.
|
|
|
18
104
|
|
|
19
105
|
## Known Limitations and Deferred Work
|
|
20
106
|
|
|
21
|
-
|
|
107
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
These limits define where the server is intentionally minimal. They are current package constraints, not a task backlog.
|
|
111
|
+
|
|
112
|
+
- **No server-wide TLS, authentication, or origin policy** — route owners such as `dsh-client-connection` enforce their own request policy. Binding a non-loopback address still exposes unprotected routes and static assets to that network.
|
|
22
113
|
- **Socket options are fixed** — config selects the bind host and port, while backlog and other socket settings remain internal until a deployment needs them.
|
|
114
|
+
|
|
115
|
+
<a id="dev-note"></a>
|
|
116
|
+
### Dev Note
|
|
117
|
+
|
|
118
|
+
<details>
|
|
119
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
120
|
+
|
|
121
|
+
None.
|
|
122
|
+
|
|
123
|
+
</details>
|
package/README.zh.md
CHANGED
|
@@ -1,22 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "web GUI 宿主的 HTTP 服务器:具名路由与 upgrade 注册、index 转换,以及服务 Web 壳 SPA dist 的唯一回退席位。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-host-webserver
|
|
2
7
|
|
|
3
8
|
[English](README.md) | 中文
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
浏览器经由 `dsh-host-webserver` 通过 HTTP 访问 web GUI:一个 `node:http` 服务器,其他插件在其中注册具名路由、upgrade 路由、index 启动输入与一个回退 handler。它不了解任何 harness 概念,也不提供任何文件服务——`/api` 桥接、插件 bundle、HMR(热模块替换)事件流与 SPA dist 都属于注册它们的插件。路由匹配顺序固定不变:先在整张表中匹配精确 route,再匹配最长前缀,最后交给回退 handler。它只服务浏览器;Electron 通过 `file://` 加载 dist,并经 IPC 桥接承载 fetch。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
27
|
+
|
|
28
|
+
把 webserver 组合为面向浏览器宿主的 HTTP 传输,然后让功能插件认领各自的路由。激活即开始监听;注册顺序不影响请求处理,因为具名路由组合起来互不相交。
|
|
29
|
+
|
|
30
|
+
### 最小配置
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
- name: '@deepseek-ai/dsh-host-webserver'
|
|
34
|
+
config:
|
|
35
|
+
host: 127.0.0.1
|
|
36
|
+
port: 3000
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`host` 只接受两个值:`127.0.0.1`(默认姿态,仅回环)与 `0.0.0.0`(有意向网络开放——服务器自身不携带 TLS、认证或来源策略)。`port` 为 0 时请求 OS 分配端口;之后用 `ctx.webServer.port` 读取正在监听的端口。
|
|
40
|
+
|
|
41
|
+
设置 `compression: 'gzip'` 可以包装符合条件的 socket-backed 响应,而不改变 route API。客户端必须接受 gzip,且媒体类型必须可压缩;已知长度小于 `compressionThresholdBytes` 的响应保持未压缩,未知长度 stream 则立即符合条件。已有编码、`Cache-Control: no-transform`、range 响应、SSE、ZIP 与已打包的 `.gz` Worker image 均保持不变。随附 Web bundle 使用 level 1 与 1024 字节阈值;其他组合默认不压缩。
|
|
42
|
+
|
|
43
|
+
### 注册路由
|
|
44
|
+
|
|
45
|
+
`register(route)` 添加具名的 `exact`/`prefix` HTTP route,`registerUpgrade(route)` 为精确 pathname 添加 upgrade route,两者返回的 disposer 都会移除注册。同一张表内的重复路径会抛错——route 模式是组合层约定,冲突即配置错误。HTTP 匹配先在整张表中匹配精确 route,再匹配最长前缀,最后交给回退 handler;upgrade 只做精确匹配,未命中连接直接关闭。
|
|
46
|
+
|
|
47
|
+
### 回退席位
|
|
48
|
+
|
|
49
|
+
`registerFallback(handler)` 认领所有未被具名 route 命中的请求的唯一个 handler。第二次注册会抛错;没有注册回退时服务器回答 404。在随附的 Web 组合中,[SPA dist 服务器](../frontend-static/README.zh.md)拥有该席位,并对其渲染的每个 index 响应调用 `renderIndex`。
|
|
50
|
+
|
|
51
|
+
index 启动输入分两层。`collectIndexInjections()` 收集一张全新的注入表——每次调用发一次 `webserver/index-inject` 事件,每个订阅方推入其当前行——`renderIndex(html)` 先把这些行渲染进 index.html 正文,再按注册顺序应用原始 `tapIndex(transform)` 转换。`script-preload` 行会渲染为 classic script 的提示性 preload 链接。静态部署会在启动 payload 中携带同一批行。`applyIndexTaps(html)` 只应用原始转换;它是任何行都无法表达的标记的逃生口。
|
|
52
|
+
|
|
53
|
+
### 失败时的行为
|
|
54
|
+
|
|
55
|
+
监听失败(例如 EADDRINUSE)会以绑定诊断信息拒绝插件初始化。handler 抛错的 HTTP 请求会得到 400——若响应头已经发出则销毁 socket——并记录 warning;它绝不会退出进程。upgrade handler 抛错或升级 socket 出现传输错误时,会记录 warning 并销毁对应 socket。
|
|
56
|
+
|
|
57
|
+
-----
|
|
6
58
|
|
|
7
|
-
|
|
59
|
+
<a id="understand-the-implementation"></a>
|
|
60
|
+
## 理解实现
|
|
8
61
|
|
|
9
|
-
|
|
62
|
+
<details>
|
|
63
|
+
<summary>实现细节——点击展开</summary>
|
|
10
64
|
|
|
65
|
+
### 设计理念
|
|
66
|
+
|
|
67
|
+
本包是一个不带任何 harness 词汇的普通路由注册表:`WebServer` 继承 Cordis `Service`,持有三张路由表、回退槽位、原始 index 转换列表,以及 index 渲染器经其收集行的 `webserver/index-inject` 事件。index 渲染每次响应组合两层:`renderIndex` 先把包含提示性 `script-preload` 行的全新注入表渲染进正文,再按注册顺序应用原始转换;`applyIndexTaps` 只运行转换。upgrade handler 拥有协议握手与连接内容;webserver 只交付原始 socket 与 request。`host` 与 `port` getter 暴露其他插件据以自适应的组合期事实(例如 directory-picker 选择器)。
|
|
68
|
+
|
|
69
|
+
### 匹配与生命周期
|
|
70
|
+
|
|
71
|
+
`match(pathname)` 先查精确表,再遍历前缀表取最长匹配,最后走回退。激活(`[Service.init]`)即开始监听;资源释放会启动 `close()` 与 `closeAllConnections()`,销毁所有受跟踪的升级 socket,并仅在服务器与这些 socket 均已关闭后返回。Node 的 `closeAllConnections()` 不包含升级 socket,因此服务显式跟踪它们。
|
|
72
|
+
|
|
73
|
+
### 源码地图
|
|
74
|
+
|
|
75
|
+
| 文件 | 职责 |
|
|
76
|
+
|---|---|
|
|
77
|
+
| [`src/index.ts`](src/index.ts) | `WebServer` 服务:路由表、回退席位、index 渲染、匹配、生命周期 |
|
|
78
|
+
| [`src/injections.ts`](src/injections.ts) | 结构化 `IndexInjection` 行与 `renderIndexInjections` 行渲染 |
|
|
79
|
+
|
|
80
|
+
</details>
|
|
81
|
+
|
|
82
|
+
-----
|
|
83
|
+
|
|
84
|
+
<a id="further-exploration"></a>
|
|
85
|
+
## 进一步探索
|
|
86
|
+
|
|
87
|
+
当服务器约定不够用时阅读以下内容:先看子系统参考,再看回退持有者,以及谁注册哪条路由背后的分层决策。
|
|
88
|
+
|
|
89
|
+
- [HTTP 服务器子系统](../../../docs/subsystems/web-server.zh.md)——路由、匹配顺序与服务器接受的配置。
|
|
90
|
+
- [SPA dist 服务器](../frontend-static/README.zh.md)——回退席位的随附持有者。
|
|
91
|
+
- [Web 配置树启动与传输分层](../../../.agents/notes/implemented/architecture/2026-07-24-web-config-tree-boot-and-transport-layering.zh.md)——功能插件为何拥有每条路由。
|
|
92
|
+
- [生成配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-host-webserver)——每个受支持配置字段及其源声明。
|
|
93
|
+
|
|
94
|
+
-----
|
|
95
|
+
|
|
96
|
+
<a id="model-experience"></a>
|
|
11
97
|
## 模型体验
|
|
12
98
|
|
|
13
|
-
|
|
99
|
+
无。该 HTTP 载体只桥接浏览器与 API handler,不注册任何面向模型的内容。
|
|
14
100
|
|
|
15
101
|
#### KV Cache 影响
|
|
16
102
|
|
|
17
103
|
无;该包既不组装也不发送提供方请求。
|
|
18
104
|
|
|
19
|
-
##
|
|
105
|
+
## 已知限制与延期工作
|
|
106
|
+
|
|
107
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
这些限制说明服务器在何处有意保持最小。它们是当前包约束,不是任务积压。
|
|
20
111
|
|
|
21
|
-
-
|
|
112
|
+
- **不提供服务器级 TLS、认证或来源策略**:`dsh-client-connection` 等 route owner 会实施自己的请求策略。绑定非回环地址仍会向该网络公开未受保护的 route 与静态资源。
|
|
22
113
|
- **Socket 选项固定不变**:配置只选择绑定宿主与端口;在具体部署产生需求前,backlog 和其他 socket 设置仍保持内部实现。
|
|
114
|
+
|
|
115
|
+
<a id="dev-note"></a>
|
|
116
|
+
### 开发备注
|
|
117
|
+
|
|
118
|
+
<details>
|
|
119
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
120
|
+
|
|
121
|
+
无。
|
|
122
|
+
|
|
123
|
+
</details>
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { Service } from "@deepseek-ai/cordis";
|
|
3
3
|
import z from "@deepseek-ai/schemastery";
|
|
4
|
+
import compressionMiddleware from "compression";
|
|
5
|
+
import Negotiator from "negotiator";
|
|
4
6
|
//#region lib/types/injections.js
|
|
5
7
|
/**
|
|
6
8
|
* Structured index injections: the typed rows plugins contribute to the boot
|
|
@@ -33,6 +35,10 @@ function renderRow(row) {
|
|
|
33
35
|
placement: row.placement,
|
|
34
36
|
markup: `<script src="${escapeHtmlAttribute(row.src)}"><\/script>`
|
|
35
37
|
};
|
|
38
|
+
case "script-preload": return {
|
|
39
|
+
placement: "head",
|
|
40
|
+
markup: `<link rel="preload" as="script" href="${escapeHtmlAttribute(row.src)}">`
|
|
41
|
+
};
|
|
36
42
|
case "style": return {
|
|
37
43
|
placement: "head",
|
|
38
44
|
markup: `<style>${row.text}</style>`
|
|
@@ -49,9 +55,18 @@ function splice(html, at, markup) {
|
|
|
49
55
|
return `${html.slice(0, at)}${markup}${html.slice(at)}`;
|
|
50
56
|
}
|
|
51
57
|
/**
|
|
58
|
+
* Tail script settling the boot-readiness deferred (`__DSH_BOOT_READY__`):
|
|
59
|
+
* the client entry awaits its `.promise` before reading any injected state.
|
|
60
|
+
* Whichever side runs first creates the deferred (`??=`), so a bootstrap that
|
|
61
|
+
* applies the table asynchronously installs it ahead of the entry module and
|
|
62
|
+
* settles it after the last row; the served form below creates and resolves
|
|
63
|
+
* it in one statement, because every row is already in the document text.
|
|
64
|
+
*/
|
|
65
|
+
const READY_MARKUP = "<script>(globalThis.__DSH_BOOT_READY__ ??= Promise.withResolvers()).resolve()<\/script>";
|
|
66
|
+
/**
|
|
52
67
|
* Render rows into an index.html body: head rows immediately after the
|
|
53
68
|
* opening head tag, body rows immediately after the opening body tag, each
|
|
54
|
-
* group in table order.
|
|
69
|
+
* group in table order, and the boot-readiness tail after the last body row.
|
|
55
70
|
* @param html - the raw index.html body.
|
|
56
71
|
* @param rows - the collected injection table.
|
|
57
72
|
* @returns the html with every row rendered.
|
|
@@ -64,6 +79,7 @@ function renderIndexInjections(html, rows) {
|
|
|
64
79
|
if (rendered.placement === "head") head += rendered.markup;
|
|
65
80
|
else body += rendered.markup;
|
|
66
81
|
}
|
|
82
|
+
body += READY_MARKUP;
|
|
67
83
|
let out = html;
|
|
68
84
|
if (head !== "") {
|
|
69
85
|
const open = /<head(?:\s[^>]*)?>/i.exec(out);
|
|
@@ -78,14 +94,40 @@ function renderIndexInjections(html, rows) {
|
|
|
78
94
|
//#endregion
|
|
79
95
|
//#region lib/types/index.js
|
|
80
96
|
/**
|
|
81
|
-
* @deepseek-ai/dsh-host-webserver —
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* Web shape only — Electron loads dist over file:// and carries fetch over an
|
|
87
|
-
* IPC bridge. This package never prints: the URL line belongs to the shell.
|
|
97
|
+
* @deepseek-ai/dsh-host-webserver — node:http route registration with optional
|
|
98
|
+
* gzip, index injection, and one fallback seat. It knows no harness concepts
|
|
99
|
+
* and serves no files; the composing application owns dist serving. Electron
|
|
100
|
+
* uses file:// plus IPC instead, and this package never prints the URL.
|
|
101
|
+
* Route handlers retain direct response ownership.
|
|
88
102
|
*/
|
|
103
|
+
const DEFAULT_COMPRESSION = "none";
|
|
104
|
+
const DEFAULT_COMPRESSION_LEVEL = 1;
|
|
105
|
+
const DEFAULT_COMPRESSION_THRESHOLD_BYTES = 1024;
|
|
106
|
+
function createGzipMiddleware(config) {
|
|
107
|
+
const middleware = compressionMiddleware({
|
|
108
|
+
level: config.compressionLevel,
|
|
109
|
+
threshold: config.compressionThresholdBytes,
|
|
110
|
+
filter(request, response) {
|
|
111
|
+
if (response.getHeader("content-range") !== void 0) return false;
|
|
112
|
+
const contentType = response.getHeader("content-type");
|
|
113
|
+
if (typeof contentType === "string" && contentType.toLowerCase().startsWith("text/event-stream")) return false;
|
|
114
|
+
return compressionMiddleware.filter(request, response);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
return (req, res, next) => {
|
|
118
|
+
if (res.socket === void 0) {
|
|
119
|
+
next();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const encoding = new Negotiator(req).encoding(["gzip", "identity"]);
|
|
123
|
+
const gzipRequest = Object.create(req);
|
|
124
|
+
Object.defineProperty(gzipRequest, "headers", { value: {
|
|
125
|
+
...req.headers,
|
|
126
|
+
"accept-encoding": encoding === "gzip" ? "gzip" : "identity"
|
|
127
|
+
} });
|
|
128
|
+
middleware(gzipRequest, res, next);
|
|
129
|
+
};
|
|
130
|
+
}
|
|
89
131
|
/**
|
|
90
132
|
* The browser HTTP carrier service. Activation listens immediately. Route
|
|
91
133
|
* registration order does not affect requests because configured named routes
|
|
@@ -97,7 +139,10 @@ var WebServer = class extends Service {
|
|
|
97
139
|
config;
|
|
98
140
|
static Config = z.object({
|
|
99
141
|
host: z.union([z.const("127.0.0.1"), z.const("0.0.0.0")]).required(),
|
|
100
|
-
port: z.natural().max(65535).required()
|
|
142
|
+
port: z.natural().max(65535).required(),
|
|
143
|
+
compression: z.union([z.const("none"), z.const("gzip")]).default(DEFAULT_COMPRESSION),
|
|
144
|
+
compressionLevel: z.number().step(1).min(0).max(9).default(DEFAULT_COMPRESSION_LEVEL),
|
|
145
|
+
compressionThresholdBytes: z.natural().default(DEFAULT_COMPRESSION_THRESHOLD_BYTES)
|
|
101
146
|
});
|
|
102
147
|
exact = /* @__PURE__ */ new Map();
|
|
103
148
|
prefixes = /* @__PURE__ */ new Map();
|
|
@@ -107,9 +152,12 @@ var WebServer = class extends Service {
|
|
|
107
152
|
fallback;
|
|
108
153
|
server;
|
|
109
154
|
listenedPort;
|
|
155
|
+
gzip;
|
|
110
156
|
constructor(ctx, config) {
|
|
111
157
|
super(ctx, "webServer");
|
|
112
158
|
this.config = config;
|
|
159
|
+
const resolved = config;
|
|
160
|
+
this.gzip = resolved.compression === "gzip" ? createGzipMiddleware(resolved) : void 0;
|
|
113
161
|
}
|
|
114
162
|
/** The listening port (the OS-assigned value when config.port is 0). */
|
|
115
163
|
get port() {
|
|
@@ -195,15 +243,19 @@ var WebServer = class extends Service {
|
|
|
195
243
|
await fallback(req, res);
|
|
196
244
|
};
|
|
197
245
|
this.server = createServer((req, res) => {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
res.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
246
|
+
const next = () => {
|
|
247
|
+
handle(req, res).catch((err) => {
|
|
248
|
+
this.ctx.logger.warn(err instanceof Error ? err : new Error(String(err)));
|
|
249
|
+
if (res.headersSent) {
|
|
250
|
+
res.destroy();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
res.writeHead(400);
|
|
254
|
+
res.end();
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
if (this.gzip === void 0) next();
|
|
258
|
+
else this.gzip(req, res, next);
|
|
207
259
|
});
|
|
208
260
|
this.server.on("upgrade", (req, socket, head) => {
|
|
209
261
|
const onError = (error) => {
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @deepseek-ai/dsh-host-webserver —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Web shape only — Electron loads dist over file:// and carries fetch over an
|
|
8
|
-
* IPC bridge. This package never prints: the URL line belongs to the shell.
|
|
2
|
+
* @deepseek-ai/dsh-host-webserver — node:http route registration with optional
|
|
3
|
+
* gzip, index injection, and one fallback seat. It knows no harness concepts
|
|
4
|
+
* and serves no files; the composing application owns dist serving. Electron
|
|
5
|
+
* uses file:// plus IPC instead, and this package never prints the URL.
|
|
6
|
+
* Route handlers retain direct response ownership.
|
|
9
7
|
*/
|
|
10
8
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
11
9
|
import type { Duplex } from 'node:stream';
|
|
@@ -46,12 +44,18 @@ export interface WebUpgradeRoute {
|
|
|
46
44
|
/** Owns protocol negotiation and the upgraded socket after dispatch. */
|
|
47
45
|
handler: (req: IncomingMessage, socket: Duplex, head: Buffer) => void | Promise<void>;
|
|
48
46
|
}
|
|
49
|
-
/**
|
|
47
|
+
/** Web server listen and response-compression config. */
|
|
50
48
|
export interface Config {
|
|
51
49
|
/** Listen host; the two supported values are loopback and all-interfaces. */
|
|
52
50
|
host: '127.0.0.1' | '0.0.0.0';
|
|
53
51
|
/** Listen port; zero requests an OS-assigned port. */
|
|
54
52
|
port: number;
|
|
53
|
+
/** Response compression for socket-backed HTTP requests. @default 'none' */
|
|
54
|
+
compression?: 'none' | 'gzip';
|
|
55
|
+
/** Gzip DEFLATE level from 0 through 9. @default 1 */
|
|
56
|
+
compressionLevel?: number;
|
|
57
|
+
/** Minimum known response length eligible for gzip; unknown-length streams are eligible. @default 1024 */
|
|
58
|
+
compressionThresholdBytes?: number;
|
|
55
59
|
}
|
|
56
60
|
/**
|
|
57
61
|
* The browser HTTP carrier service. Activation listens immediately. Route
|
|
@@ -71,6 +75,7 @@ export declare class WebServer extends Service {
|
|
|
71
75
|
private fallback;
|
|
72
76
|
private server;
|
|
73
77
|
private listenedPort;
|
|
78
|
+
private readonly gzip;
|
|
74
79
|
constructor(ctx: Context, config: Config);
|
|
75
80
|
/** The listening port (the OS-assigned value when config.port is 0). */
|
|
76
81
|
get port(): number;
|
|
@@ -33,6 +33,11 @@ export type IndexInjection =
|
|
|
33
33
|
placement: IndexInjectionPlacement;
|
|
34
34
|
src: string;
|
|
35
35
|
}
|
|
36
|
+
/** Advisory preload for an external classic script; static workers may ignore it. */
|
|
37
|
+
| {
|
|
38
|
+
kind: 'script-preload';
|
|
39
|
+
src: string;
|
|
40
|
+
}
|
|
36
41
|
/** A `<style>` element in the head. `text` must not contain `</style`, which would close the element early. */
|
|
37
42
|
| {
|
|
38
43
|
kind: 'style';
|
|
@@ -47,7 +52,7 @@ export type IndexInjection =
|
|
|
47
52
|
/**
|
|
48
53
|
* Render rows into an index.html body: head rows immediately after the
|
|
49
54
|
* opening head tag, body rows immediately after the opening body tag, each
|
|
50
|
-
* group in table order.
|
|
55
|
+
* group in table order, and the boot-readiness tail after the last body row.
|
|
51
56
|
* @param html - the raw index.html body.
|
|
52
57
|
* @param rows - the collected injection table.
|
|
53
58
|
* @returns the html with every row rendered.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-host-webserver",
|
|
3
3
|
"description": "Web route-registration plugin: HTTP and upgrade routes, index transform taps, and static dist fallback; knows no harness concepts",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -32,14 +32,18 @@
|
|
|
32
32
|
],
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@deepseek-ai/
|
|
36
|
-
"@deepseek-ai/
|
|
35
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
36
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"
|
|
39
|
+
"compression": "^1.8.1",
|
|
40
|
+
"negotiator": "^1.0.0",
|
|
41
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
40
42
|
},
|
|
41
43
|
"devDependencies": {
|
|
42
|
-
"@
|
|
43
|
-
"@
|
|
44
|
+
"@types/compression": "^1.8.1",
|
|
45
|
+
"@types/negotiator": "^0.6.5",
|
|
46
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
47
|
+
"@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2"
|
|
44
48
|
}
|
|
45
49
|
}
|