fix(web): address review on harness dev context

- WebServer exposes getOrigin() instead of an origin getter, and it fails
  loud before listen binds a port.
- The earlier Web-surface paragraph is a frozen literal, so later guidance
  edits cannot stop earlier Sessions from matching; the projection reads a
  prompt version with the loop's single-text-block rule.
- web-app drops its unused dsh-system-prompt dependency and stale
  prompt-section wording in its description, README, patch comment,
  --public-url help, and the round-trip test title.
This commit is contained in:
imccyu committed 2026-10-11 23:25:24 +08:00
1 parent b10a605e6b
commit adfa92bca1
29 files changed
+115 -71

No files matched your search

@@ -12,8 +12,8 @@
en: 5222f00fd05d3801
zh: fcfa430a0fa5588c
/agent-note-process-specific-checkout-path-and-web-url-move-to-runtime-context/decision/ownership:
en: 819556fd85d945dd
zh: 7c346c261e2940a1
en: a98a00946a0dbaf0
zh: c272d6bf29c08ef8
/agent-note-process-specific-checkout-path-and-web-url-move-to-runtime-context/decision/new-sessions:
en: 1ee144c39905bb89
zh: a6669d7ad394e41c
@@ -27,5 +27,5 @@
en: dc4c15a88ac09de0
zh: 0d65b4b3f2471d5c
/agent-note-process-specific-checkout-path-and-web-url-move-to-runtime-context/testing:
en: 0f2be07092471cdc
zh: bd2771ca1b970be8
en: 34e1b557ce2d7bef
zh: 069bb16cab1a659c
@@ -25,11 +25,11 @@ Both values become runtime context and the system prompt keeps only value-free g
| Package | Responsibility |
|---|---|
| `@deepseek-ai/dsh-harness-dev-context` (`packages/context/harness-dev-context`) | Both sections, both contexts, and the earlier-format projection; all module-private, exporting only the plugin protocol (`name`, `inject`, `apply`) and the projection state type `LegacyPrompt` |
| `@deepseek-ai/dsh-host-webserver` | `WebServer.origin` (the bind address's URL form with protocol and bound port) and `bindUrlHost(host)` |
| `@deepseek-ai/dsh-host-webserver` | `WebServer.getOrigin()` (the bind address's URL form with protocol and bound port) and `bindUrlHost(host)` |
| `@deepseek-ai/dsh-web-app` | The `web-startup` plugin provides `webStartup` and publishes a canonical `publicUrl`; the package root declares `Context.webStartup`; the bundle patch mounts the `harness-dev-context` row; the runtime plugin keeps only `DSH_WEB_URL`, the startup URL, and the browser handoff |
| `@deepseek-ai/dsh-system-prompt` | `CONTEXT_ORDERS.HARNESS_SOURCE = 200` and `WEB_SURFACE = 210` |
The plugin injects `systemPrompt`, `sessionProjections`, `webServer`, and `webStartup` and reads those services in code; its patch row carries no configuration expression. The checkout path is four directories above this package's source or built entry, the same depth as the former web-app location, so source, npm, and Desktop installations resolve the same root. The URL is `webStartup.publicUrl`, or `webServer.origin` without one. The plugin reaches the `webStartup` type through `import type {} from '@deepseek-ai/dsh-web-app'`, the package root that declares it; the import is erased at runtime, so the web-app bundle's dependency on this package remains the only runtime edge.
The plugin injects `systemPrompt`, `sessionProjections`, `webServer`, and `webStartup` and reads those services in code; its patch row carries no configuration expression. The checkout path is four directories above this package's source or built entry, the same depth as the former web-app location, so source, npm, and Desktop installations resolve the same root. The URL is `webStartup.publicUrl`, or `webServer.getOrigin()` without one. The plugin reaches the `webStartup` type through `import type {} from '@deepseek-ai/dsh-web-app'`, the package root that declares it; the import is erased at runtime, so the web-app bundle's dependency on this package remains the only runtime edge.
### New Sessions
@@ -90,6 +90,6 @@ The projection computes only on each committed `system/message`, adding no synch
## Testing
- `packages/context/harness-dev-context/tests/harness-dev-context.spec.ts`: on a production AgentLoop, an earlier-format Session resumed on another port emits no `system/message`, the request's system prompt is byte-identical to the log, and runtime context carries the current URL; a second port change keeps this; replace-only and in-history routes each run. When a series consolidation only empties a later version, the next turn keeps the earlier paragraphs. Similar text without the fixed ending or broken across lines receives the current guidance. A new Session keeps its system prompt across a port change and only appends a runtime-context snapshot. Sections carry no values, the context carries the origin or `publicUrl`, and disposing the plugin fiber removes every registration.
- `packages/host/webserver/tests/webserver.spec.ts`: `bindUrlHost` across address forms; `origin` on HTTP and HTTPS listeners, and its error for a zoned non-loopback address.
- `packages/host/webserver/tests/webserver.spec.ts`: `bindUrlHost` across address forms; `getOrigin()` on HTTP and HTTPS listeners, and its errors before binding and for a zoned non-loopback address.
- `packages/bundle/web-app/tests/startup.spec.ts`: `webStartup.publicUrl` publishes the canonical form.
- `packages/core/system-prompt/tests/system-prompt.spec.ts`: the added context positions are unique integers.
@@ -25,11 +25,11 @@ Status: implemented
| 包 | 职责 |
|---|---|
| `@deepseek-ai/dsh-harness-dev-context`(`packages/context/harness-dev-context`) | 两个 section、两个 context、旧格式投影;全部模块私有,只导出插件协议(`name`、`inject`、`apply`)与投影状态类型 `LegacyPrompt` |
| `@deepseek-ai/dsh-host-webserver` | `WebServer.origin`(绑定地址的 URL 形式加协议与实际端口)与 `bindUrlHost(host)` |
| `@deepseek-ai/dsh-host-webserver` | `WebServer.getOrigin()`(绑定地址的 URL 形式加协议与实际端口)与 `bindUrlHost(host)` |
| `@deepseek-ai/dsh-web-app` | `web-startup` 插件提供 `webStartup`,并发布规范化的 `publicUrl`;包根声明 `Context.webStartup`;bundle patch 挂载 `harness-dev-context` 行;运行时插件只保留 `DSH_WEB_URL`、启动 URL 与浏览器交接 |
| `@deepseek-ai/dsh-system-prompt` | `CONTEXT_ORDERS.HARNESS_SOURCE = 200`、`WEB_SURFACE = 210` |
插件 `inject` 为 `systemPrompt`、`sessionProjections`、`webServer`、`webStartup`,在代码里直接读取这些服务;patch 行不写任何配置表达式。checkout 路径取本包源码或构建入口上溯四层,与原 web-app 位置同深度,源码、npm 与 Desktop 安装下结果不变。URL 取 `webStartup.publicUrl`,没有时取 `webServer.origin`。插件通过 `import type {} from '@deepseek-ai/dsh-web-app'` 从声明它的包根取得 `webStartup` 类型;该导入在运行时被擦除,因此 web-app bundle 对本包的依赖仍是唯一的运行时依赖。
插件 `inject` 为 `systemPrompt`、`sessionProjections`、`webServer`、`webStartup`,在代码里直接读取这些服务;patch 行不写任何配置表达式。checkout 路径取本包源码或构建入口上溯四层,与原 web-app 位置同深度,源码、npm 与 Desktop 安装下结果不变。URL 取 `webStartup.publicUrl`,没有时取 `webServer.getOrigin()`。插件通过 `import type {} from '@deepseek-ai/dsh-web-app'` 从声明它的包根取得 `webStartup` 类型;该导入在运行时被擦除,因此 web-app bundle 对本包的依赖仍是唯一的运行时依赖。
### 新会话
@@ -90,6 +90,6 @@ Status: implemented
## 测试
- `packages/context/harness-dev-context/tests/harness-dev-context.spec.ts`:真实 AgentLoop 上,旧格式会话在另一端口恢复时不产生 `system/message`、请求中的 system prompt 与日志逐字相同、运行时上下文携带当前 URL;再换一次端口仍成立;不支持与支持 in-history 的路由各跑一遍。series 合并只清空后续版本时,下一轮仍沿用旧段落。缺少固定结尾或跨行的相似文本得到新说明。新会话换端口后 system prompt 不变、只追加运行时上下文快照。section 不含值、context 携带 origin 或 `publicUrl`,销毁插件 fiber 后全部注册消失。
- `packages/host/webserver/tests/webserver.spec.ts`:`bindUrlHost` 各地址形式;`origin` 在 HTTP 与 HTTPS 监听上的值,带 zone 的非回环地址抛错。
- `packages/host/webserver/tests/webserver.spec.ts`:`bindUrlHost` 各地址形式;`getOrigin()` 在 HTTP 与 HTTPS 监听上的值,绑定前与带 zone 的非回环地址抛错。
- `packages/bundle/web-app/tests/startup.spec.ts`:`webStartup.publicUrl` 发布规范化形式。
- `packages/core/system-prompt/tests/system-prompt.spec.ts`:新增的 context 位置唯一且为整数。
+1 -1
View File
@@ -213,7 +213,7 @@ describe('web e2e: fresh round trip through the real assembly', () => {
}
}, 200_000)
it('keeps source checkout and Web context in the system prompt and cwd in user context', async () => {
it('keeps value-free Web guidance in the system prompt and process values in runtime context', async () => {
if (settledSessionId === undefined) throw new Error('the drive turn did not publish a session id')
const agent = scaffold.ctx.agents.get(settledSessionId)
if (agent === undefined) throw new Error(`the settled Web agent ${settledSessionId} is no longer live`)
+1 -1
View File
@@ -180,7 +180,7 @@ Options:
--port <port> listen port; pass 0 to let the OS pick a free
one
--public-url <url> advertise this HTTP(S) root in the printed,
opened, web-surface, and DSH_WEB_URL forms;
opened, runtime-context, and DSH_WEB_URL forms;
grants no trust
--trusted-host <authority...> extra authority the /api browser-trust fence
accepts (host or host:port; repeatable)
+1 -1
View File
@@ -182,7 +182,7 @@ Options:
--port <port> listen port; pass 0 to let the OS pick a free
one
--public-url <url> advertise this HTTP(S) root in the printed,
opened, web-surface, and DSH_WEB_URL forms;
opened, runtime-context, and DSH_WEB_URL forms;
grants no trust
--trusted-host <authority...> extra authority the /api browser-trust fence
accepts (host or host:port; repeatable)
+1 -2
View File
@@ -1045,7 +1045,6 @@ flowchart TD
pkg_api_settings_controller --> pkg_settings
pkg_api_settings_controller --> pkg_typert_protocol
pkg_web_app --> pkg_shell_env
pkg_web_app --> pkg_system_prompt
pkg_experimental_auto_review --> pkg_agent
pkg_experimental_auto_review --> pkg_agent_instructions
pkg_experimental_auto_review --> pkg_llm
@@ -1720,7 +1719,7 @@ flowchart TD
| [`token-meter`](../packages/llm/token-meter) | `llm` | [`compaction`](../packages/compaction/compaction), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-title`](../packages/session/session-title), [`tool-todo`](../packages/todo/tool-todo) |
| [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`credentials`](../packages/credentials/credentials), [`native-command`](../packages/util/native-command), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) |
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) |
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`shell-env`](../packages/shell/shell-env) |
| [`experimental-auto-review`](../packages/experimental/auto-review) | `experimental` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
| [`webhook`](../packages/experimental/webhook) | `experimental` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) |
| [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`app-boot`](../packages/boot/app-boot), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`tools`](../packages/core/tools) |
+1 -2
View File
@@ -1047,7 +1047,6 @@ flowchart TD
pkg_api_settings_controller --> pkg_settings
pkg_api_settings_controller --> pkg_typert_protocol
pkg_web_app --> pkg_shell_env
pkg_web_app --> pkg_system_prompt
pkg_experimental_auto_review --> pkg_agent
pkg_experimental_auto_review --> pkg_agent_instructions
pkg_experimental_auto_review --> pkg_llm
@@ -1722,7 +1721,7 @@ flowchart TD
| [`token-meter`](../packages/llm/token-meter) | `llm` | [`compaction`](../packages/compaction/compaction), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
| [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-title`](../packages/session/session-title), [`tool-todo`](../packages/todo/tool-todo) |
| [`api-settings-controller`](../packages/api/settings-controller) | `api` | [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`credentials`](../packages/credentials/credentials), [`native-command`](../packages/util/native-command), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) |
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) |
| [`web-app`](../packages/bundle/web-app) | `bundle` | [`shell-env`](../packages/shell/shell-env) |
| [`experimental-auto-review`](../packages/experimental/auto-review) | `experimental` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) |
| [`webhook`](../packages/experimental/webhook) | `experimental` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-preset-registry`](../packages/preset/agent-preset-registry), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) |
| [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`app-boot`](../packages/boot/app-boot), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`tools`](../packages/core/tools) |
+8
View File
@@ -150,6 +150,14 @@ Source: [`packages/client/connection/src/rpc.ts`](../../packages/client/connecti
The browser HTTP carrier service. Activation loads any configured TLS material, then listens immediately; a material or listen failure rejects initialization, and the boot process reports the failed fiber. Route registration order does not affect requests because configured named routes must be distinct, and the fallback handler answers anything not yet claimed during startup with 404 until its owner registers.
```ts cordis-catalog
/**
* The root URL local clients use to reach the bound listener: the protocol,
* the {@link bindUrlHost} form of the bind address, and the listening port.
* @returns the origin, for example `http://127.0.0.1:3080`.
* @throws before `listen` binds a port, or when the bind address has no URL form.
*/
getOrigin(): string
/**
* Register a named route. Duplicate (kind, path) throws — route patterns are
* a composition-level contract, so a collision is a misconfiguration.
+8
View File
@@ -150,6 +150,14 @@ Source: [`packages/client/connection/src/rpc.ts`](../../packages/client/connecti
The browser HTTP carrier service. Activation loads any configured TLS material, then listens immediately; a material or listen failure rejects initialization, and the boot process reports the failed fiber. Route registration order does not affect requests because configured named routes must be distinct, and the fallback handler answers anything not yet claimed during startup with 404 until its owner registers.
```ts cordis-catalog
/**
* The root URL local clients use to reach the bound listener: the protocol,
* the {@link bindUrlHost} form of the bind address, and the listening port.
* @returns the origin, for example `http://127.0.0.1:3080`.
* @throws before `listen` binds a port, or when the bind address has no URL form.
*/
getOrigin(): string
/**
* Register a named route. Duplicate (kind, path) throws — route patterns are
* a composition-level contract, so a collision is a misconfiguration.
+4 -4
View File
@@ -33,8 +33,8 @@
en: ccd6fc264b51e44b
zh: 3265c68477a89ec9
/deepseek-ai-dsh-web-app/understand-the-implementation:
en: 2ea88398028ebff8
zh: 0e589c297aeb9d43
en: 323742a00cfe67b2
zh: eff5245a82fc16e7
/deepseek-ai-dsh-web-app/understand-the-implementation/patch-semantics:
en: 8a2554b3d17daa1f
zh: 66f18946791cabb6
@@ -45,8 +45,8 @@
en: 973128fd346869df
zh: 54e6792f9f82a192
/deepseek-ai-dsh-web-app/understand-the-implementation/source-map:
en: d4c7f39a9fd90683
zh: ac2dbf75b2ceb255
en: 58800b6f2142c34f
zh: 10bf512b16db1cb8
/deepseek-ai-dsh-web-app/further-exploration:
en: f81694f7424e2988
zh: 6643f9227039e228
+3 -3
View File
@@ -96,7 +96,7 @@ Each browser session selects a shipped preset (`standard` by default). The Agent
<details>
<summary>Implementation internals — click to expand</summary>
The bundle is one patch layer of five files plus one runtime glue plugin: `cordis.patch.yml` carries the host rows and the preset registry, and each `presets/<id>.patch.yml` inserts one shipped preset declaration, applied in the order `dsh.bundle.patch` lists them. The storage stack and projection cache come from `dsh-base`; the web overlay's workspace and message-feedback rows consume that shared `storageDomain` service. The patch restates the surface-specific values the base deliberately omits, inserts the web-only host rows and browser roster, then moves the agent plane behind presets. The glue plugin owns dist serving, the advertised application URL, the plain-HTTP exposure warning, prompt sections, the bash variable, and the readiness announcements. The `office-to-pdf` row mounts one lazy [Office conversion provider](../../document/office-to-pdf/README.md) for Host consumers, including Desktop compositions using this bundle. The conversion service's Remote methods authorize preview reads, while Document Preview owns the Office viewer and Client cache.
The bundle is one patch layer of five files plus one runtime glue plugin: `cordis.patch.yml` carries the host rows and the preset registry, and each `presets/<id>.patch.yml` inserts one shipped preset declaration, applied in the order `dsh.bundle.patch` lists them. The storage stack and projection cache come from `dsh-base`; the web overlay's workspace and message-feedback rows consume that shared `storageDomain` service. The patch restates the surface-specific values the base deliberately omits, inserts the web-only host rows and browser roster, then moves the agent plane behind presets. The glue plugin owns dist serving, the advertised application URL, the plain-HTTP exposure warning, the bash variable, and the readiness announcements. The `office-to-pdf` row mounts one lazy [Office conversion provider](../../document/office-to-pdf/README.md) for Host consumers, including Desktop compositions using this bundle. The conversion service's Remote methods authorize preview reads, while Document Preview owns the Office viewer and Client cache.
### Patch semantics
@@ -114,12 +114,12 @@ The URL line and browser handoff are readiness signals: supervisors RPC as soon
| File | Role |
|---|---|
| [`src/index.ts`](src/index.ts) | The `web-app` glue plugin: dist resolution, advertised application URL, prompt sections, bash variable, URL line, browser handoff |
| [`src/index.ts`](src/index.ts) | The `web-app` glue plugin: dist resolution, advertised application URL, bash variable, URL line, browser handoff |
| [`src/public-url.ts`](src/public-url.ts) | Advertised-root validation and trailing-slash normalization; a leaf module for local imports, not package API |
| [`src/startup.ts`](src/startup.ts) | The `web-startup` provider: `--host`, `--port`, `--tls-cert`, `--tls-key`, `--public-url`, `--trusted-host`, `--no-open`, `--help` |
| [`cordis.patch.yml`](cordis.patch.yml) | The web patch: restated base values, web host rows, browser roster, preset registry |
| [`presets/`](presets) | One `@deepseek-ai/dsh-agent-preset` declaration per shipped preset (`standard`, `ptc`, `minimal`, `cordis`), each its own patch file |
| [`tests/web-app.spec.ts`](tests/web-app.spec.ts) | Dist index anchoring, advertised-URL publication and warnings, prompt sections, readiness publication |
| [`tests/web-app.spec.ts`](tests/web-app.spec.ts) | Dist index anchoring, advertised-URL publication and warnings, bash variable, readiness publication |
| [`tests/startup.spec.ts`](tests/startup.spec.ts) | Command-line parsing over a real Loader tree |
| [`tests/public-url.spec.ts`](tests/public-url.spec.ts) | Advertised-root parsing and normalization |
| [`tests/browser-open.spec.ts`](tests/browser-open.spec.ts) | Default-browser handoff after the page is reachable |
+3 -3
View File
@@ -96,7 +96,7 @@ dsh --profile web --no-open --host "$(hostname -i | awk '{print $1}')" --public-
<details>
<summary>实现细节——点击展开</summary>
此 bundle 由一层五个文件的补丁和一个运行时胶水插件组成:`cordis.patch.yml` 承载宿主行和 preset 注册表,每个 `presets/<id>.patch.yml` 插入一条随发行版交付的 preset 声明,按 `dsh.bundle.patch` 列出的顺序应用。存储栈与投影缓存来自 `dsh-base`;Web 叠加层的工作区和消息反馈条目消费共享的 `storageDomain` 服务。补丁重述 base 有意省略的界面专用值,插入 Web 专用宿主条目和浏览器插件列表,再将 Agent 层移到预设后面。胶水插件负责 dist 服务、公告应用 URL、明文 HTTP 暴露警告、提示词段落、bash 变量和就绪通知。`office-to-pdf` 条目为宿主消费者挂载一个延迟创建引擎的 [Office 转换提供方](../../document/office-to-pdf/README.zh.md),使用此 bundle 的 Desktop 组合也共享该提供方。 转换服务的 Remote 方法负责预览读取授权,Document Preview 负责 Office 查看器和客户端缓存。
此 bundle 由一层五个文件的补丁和一个运行时胶水插件组成:`cordis.patch.yml` 承载宿主行和 preset 注册表,每个 `presets/<id>.patch.yml` 插入一条随发行版交付的 preset 声明,按 `dsh.bundle.patch` 列出的顺序应用。存储栈与投影缓存来自 `dsh-base`;Web 叠加层的工作区和消息反馈条目消费共享的 `storageDomain` 服务。补丁重述 base 有意省略的界面专用值,插入 Web 专用宿主条目和浏览器插件列表,再将 Agent 层移到预设后面。胶水插件负责 dist 服务、公告应用 URL、明文 HTTP 暴露警告、bash 变量和就绪通知。`office-to-pdf` 条目为宿主消费者挂载一个延迟创建引擎的 [Office 转换提供方](../../document/office-to-pdf/README.zh.md),使用此 bundle 的 Desktop 组合也共享该提供方。 转换服务的 Remote 方法负责预览读取授权,Document Preview 负责 Office 查看器和客户端缓存。
### patch 语义
@@ -114,12 +114,12 @@ URL 行与浏览器交接都是就绪信号:监督方一观察到该行就发
| 文件 | 职责 |
|---|---|
| [`src/index.ts`](src/index.ts) | `web-app` 粘合插件:dist 解析、公告应用 URL、提示词段落、bash 变量、URL 行、浏览器交接 |
| [`src/index.ts`](src/index.ts) | `web-app` 粘合插件:dist 解析、公告应用 URL、bash 变量、URL 行、浏览器交接 |
| [`src/public-url.ts`](src/public-url.ts) | 公告根的校验与尾斜杠归一化;供本地导入的叶子模块,不属于包 API |
| [`src/startup.ts`](src/startup.ts) | `web-startup` 提供方:`--host`、`--port`、`--tls-cert`、`--tls-key`、`--public-url`、`--trusted-host`、`--no-open`、`--help` |
| [`cordis.patch.yml`](cordis.patch.yml) | Web patch:重述的基础值、Web 宿主行、浏览器名录、preset 注册表 |
| [`presets/`](presets) | 每个随发行版交付的 preset(`standard`、`ptc`、`minimal`、`cordis`)各一条 `@deepseek-ai/dsh-agent-preset` 声明,各自一个补丁文件 |
| [`tests/web-app.spec.ts`](tests/web-app.spec.ts) | dist index 锚定、公告 URL 发布与警告、提示词段落、就绪宣告发布 |
| [`tests/web-app.spec.ts`](tests/web-app.spec.ts) | dist index 锚定、公告 URL 发布与警告、bash 变量、就绪宣告发布 |
| [`tests/startup.spec.ts`](tests/startup.spec.ts) | 在真实 Loader 树上的命令行解析 |
| [`tests/public-url.spec.ts`](tests/public-url.spec.ts) | 公告根的解析与归一化 |
| [`tests/browser-open.spec.ts`](tests/browser-open.spec.ts) | 页面可达后的默认浏览器交接 |
+3 -2
View File
@@ -205,8 +205,9 @@
# Model context for an agent that inspects or extends the harness serving
# this GUI: value-free prompt sections plus the current checkout and GUI
# URL as runtime context. A complete agent-preset persona suppresses the
# sections for that agent.
# URL as optional runtime context. A complete agent-preset persona replaces
# the sections, and a persona that suppresses optional runtime context
# also drops the two value lines.
- id: harness-dev-context
name: '@deepseek-ai/dsh-harness-dev-context'
+2 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-web-app",
"description": "The dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)",
"description": "The dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, bash runtime variables, URL line)",
"version": "0.2.1-alpha.2",
"publishConfig": {
"access": "public"
@@ -190,8 +190,7 @@
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/cordis-plugin-loader": "workspace:~",
"@deepseek-ai/dsh-shell-env": "workspace:*",
"@deepseek-ai/dsh-system-prompt": "workspace:*"
"@deepseek-ai/dsh-shell-env": "workspace:*"
},
"devDependencies": {
"@deepseek-ai/cordis": "workspace:~",
@@ -203,7 +202,6 @@
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-session-projection": "workspace:*",
"@deepseek-ai/dsh-shell-env": "workspace:*",
"@deepseek-ai/dsh-system-prompt": "workspace:*",
"@deepseek-ai/dsh-typert-registry": "workspace:*",
"@deepseek-ai/libreoffice-kit": "^0.1.5",
"@deepseek-ai/dsh-otel": "workspace:*"
+1 -1
View File
@@ -193,7 +193,7 @@ export function apply(ctx: Context, config: Config): void {
// through a local forwarding address that this process cannot derive.
const handoffBrowser = config.openBrowser && !launchedThroughSsh(launchEnvironmentOf(ctx))
/** The advertised root: the canonical `publicUrl`, or the listener's own origin. */
const appRootUrl = (): string => publicUrl ?? ctx.webServer.origin
const appRootUrl = (): string => publicUrl ?? ctx.webServer.getOrigin()
ctx.plugin(FrontendStatic, { distIndex: internals.resolveDistIndex() })
if (config.surfaceContext) {
ctx.inject(['shellEnv'], (runtimeCtx) => {
+1 -1
View File
@@ -65,7 +65,7 @@ function webCommand(): Command {
.option('--host <host>', 'bind address: one concrete IPv4 or IPv6 literal of a local interface; wildcard addresses are rejected')
.option('--no-open', 'do not open the Web UI in the default browser')
.option('--port <port>', 'listen port; pass 0 to let the OS pick a free one')
.option('--public-url <url>', 'advertise this HTTP(S) root in the printed, opened, web-surface, and DSH_WEB_URL forms; grants no trust')
.option('--public-url <url>', 'advertise this HTTP(S) root in the printed, opened, runtime-context, and DSH_WEB_URL forms; grants no trust')
.option('--trusted-host <authority...>', 'extra authority the /api browser-trust fence accepts (host or host:port; repeatable)')
.option('--tls-cert <file>', 'PEM server certificate chain; requires --tls-key')
.option('--tls-key <file>', 'unencrypted PEM private key; requires --tls-cert')
@@ -58,11 +58,11 @@ function stageDist(): string {
/** Web carrier without a network listener. */
function fakeHttpServer(host = '127.0.0.1'): { server: WebServer } {
const server: Pick<WebServer, 'host' | 'port' | 'protocol' | 'origin' | 'registerFallback' | 'renderIndex'> = {
const server: Pick<WebServer, 'host' | 'port' | 'protocol' | 'getOrigin' | 'registerFallback' | 'renderIndex'> = {
host,
port: 4567,
protocol: 'http:',
get origin() { return `http://${bindUrlHost(host) ?? ''}:${String(this.port)}` },
getOrigin() { return `http://${bindUrlHost(host) ?? ''}:${String(this.port)}` },
registerFallback: () => () => {},
renderIndex: (html: string) => html,
}
-3
View File
@@ -35,9 +35,6 @@
{
"path": "../../util/launch-environment"
},
{
"path": "../../core/system-prompt"
},
{
"path": "../../shell/shell-env"
},
@@ -52,8 +52,10 @@ const WEB_SURFACE_GUIDANCE = 'When the user refers to "this page", "this GUI", o
const WEB_SURFACE_PROMPT = `You are interacting with the user through the DeepSeek Harness Web GUI whose URL the runtime context names. ${WEB_SURFACE_GUIDANCE}`
/**
* Fixed text around the value in each paragraph that embedded it. The source
* suffix keeps its original wording, which differs from the current guidance.
* Fixed text around the value in each paragraph that embedded it, frozen as
* those releases rendered it: never edit these together with the current
* guidance, or Sessions holding the earlier paragraphs stop matching and
* rewrite their logged prompt.
*/
const LEGACY_SOURCE = {
prefix: 'The DeepSeek Harness implementation checkout is at ',
@@ -61,10 +63,20 @@ const LEGACY_SOURCE = {
+ 'never infer the working directory from this path. Use pwd to determine the current working directory. '
+ 'Use this checkout only to inspect or extend DSH itself.',
}
// The frozen copy repeats today's guidance on purpose; the two may diverge.
/* jscpd:ignore-start */
const LEGACY_WEB_SURFACE = {
prefix: 'You are interacting with the user through the DeepSeek Harness Web GUI at ',
suffix: `. ${WEB_SURFACE_GUIDANCE}`,
suffix: '. When the user refers to "this page", "this GUI", or "this app" without naming another target, they mean this GUI. '
+ 'The browser provides no implicit DOM, route, or screenshot context. '
+ 'The client-plugin HMR receiver is active, but client-plugin changes reload without a refresh only while '
+ '`pnpm run dev:web` is also running from this same checkout to rebuild their bundles; verify that watcher before promising automatic updates. '
+ 'Every other change — the apps/web shell and plain packages — requires rebuilding the affected Web artifacts and verifying this existing URL after a page refresh. '
+ 'Starting another server does not update this GUI. '
+ 'The apps/web Vite entry builds the shell but is not a standalone application because only dsh web injects window.__DSH_BOOT__. '
+ 'Do not start a replacement server unless the user asks; if one is needed, use a managed background job and verify its exact URL.',
}
/* jscpd:ignore-end */
/** Host-only projection key of the value-bearing paragraphs a Session admitted. */
const LEGACY_PROMPT = 'harnessDevLegacyPrompt'
@@ -91,11 +103,17 @@ function legacyParagraph(prompt: string, { prefix, suffix }: { prefix: string; s
return paragraph.includes('\n') ? null : paragraph
}
/** Fold one committed event into the admitted paragraphs; empty prompt versions admit nothing. */
/**
* Fold one committed event into the admitted paragraphs. Like the loop's own
* prompt-version reading, only a single nonempty text block is a prompt
* version; an emptied or other version keeps the previous state, because a
* series consolidation can log only the emptied later version.
*/
function foldLegacyPrompt(state: LegacyPrompt, event: SessionEvent): LegacyPrompt {
if (event.type !== 'system/message') return state
const [block] = event.data.message.content
if (block?.type !== 'text' || block.text === '') return state
const { content } = event.data.message
const [block] = content
if (content.length !== 1 || block?.type !== 'text' || block.text === '') return state
const source = legacyParagraph(block.text, LEGACY_SOURCE)
const web = legacyParagraph(block.text, LEGACY_WEB_SURFACE)
return source === state.source && web === state.web ? state : { source, web }
@@ -143,6 +161,6 @@ export function apply(ctx: Context): void {
name: WEB_SURFACE,
order: ctx.systemPrompt.getContextOrder('WEB_SURFACE'),
interpolate: false,
text: () => `Current DeepSeek Harness Web GUI URL: ${JSON.stringify(ctx.webStartup.publicUrl ?? ctx.webServer.origin)}.`,
text: () => `Current DeepSeek Harness Web GUI URL: ${JSON.stringify(ctx.webStartup.publicUrl ?? ctx.webServer.getOrigin())}.`,
})
}
@@ -26,7 +26,16 @@ const WEB_PROMPT = `You are interacting with the user through the DeepSeek Harne
/** The value-bearing paragraphs earlier releases rendered. */
const LEGACY_SOURCE = 'The DeepSeek Harness implementation checkout is at /old/checkout/. The checkout location and current working directory are separate values and may differ; never infer the working directory from this path. Use pwd to determine the current working directory. Use this checkout only to inspect or extend DSH itself.'
const LEGACY_WEB = `You are interacting with the user through the DeepSeek Harness Web GUI at http://127.0.0.1:3100. ${WEB_GUIDANCE}`
// Frozen as earlier releases rendered it, independent of the current guidance.
const LEGACY_WEB = 'You are interacting with the user through the DeepSeek Harness Web GUI at http://127.0.0.1:3100. '
+ 'When the user refers to "this page", "this GUI", or "this app" without naming another target, they mean this GUI. '
+ 'The browser provides no implicit DOM, route, or screenshot context. '
+ 'The client-plugin HMR receiver is active, but client-plugin changes reload without a refresh only while '
+ '`pnpm run dev:web` is also running from this same checkout to rebuild their bundles; verify that watcher before promising automatic updates. '
+ 'Every other change — the apps/web shell and plain packages — requires rebuilding the affected Web artifacts and verifying this existing URL after a page refresh. '
+ 'Starting another server does not update this GUI. '
+ 'The apps/web Vite entry builds the shell but is not a standalone application because only dsh web injects window.__DSH_BOOT__. '
+ 'Do not start a replacement server unless the user asks; if one is needed, use a managed background job and verify its exact URL.'
/** Records every request and answers each with one short text reply. */
class RecordingAdapter extends LlmAdapter {
@@ -59,8 +68,8 @@ const ROUTE = { provider: 'route', model: 'model' }
function provideWebServices(ctx: Context, startup: { publicUrl?: string } = {}): { port: number } {
const listener = { port: 4567 }
ctx.provide('webServer', {
get origin() { return `http://127.0.0.1:${String(listener.port)}` },
} as Pick<WebServer, 'origin'> as WebServer)
getOrigin: () => `http://127.0.0.1:${String(listener.port)}`,
} as Pick<WebServer, 'getOrigin'> as WebServer)
ctx.provide('webStartup', { openBrowser: false, trustedHosts: [], ...startup } satisfies Context['webStartup'])
return listener
}
@@ -3578,6 +3578,13 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
summary: 'The browser HTTP carrier service.',
description: 'The browser HTTP carrier service. Activation loads any configured TLS material, then listens immediately; a material or listen failure rejects initialization, and the boot process reports the failed fiber. Route registration order does not affect requests because configured named routes must be distinct, and the fallback handler answers anything not yet claimed during startup with 404 until its owner registers.',
methods: [
{
signature: 'getOrigin(): string',
description: 'The root URL local clients use to reach the bound listener: the protocol, the bindUrlHost form of the bind address, and the listening port.',
parameters: [],
returns: 'the origin, for example `http://127.0.0.1:3080`.',
throws: ['before `listen` binds a port, or when the bind address has no URL form.'],
},
{
signature: 'register(route: WebRoute): () => void',
description: 'Register a named route. Duplicate (kind, path) throws — route patterns are a composition-level contract, so a collision is a misconfiguration.',
+4 -4
View File
@@ -18,8 +18,8 @@
en: 1998834973f7cf17
zh: 63455a2f75d090ad
/deepseek-ai-dsh-host-webserver/use-this-package/minimal-configuration:
en: a647f72ef05eeddd
zh: af7d66d892575c42
en: eeb4865ed96338a5
zh: 08138a132e633e54
/deepseek-ai-dsh-host-webserver/use-this-package/serving-https:
en: 90352948bfdc1d99
zh: 35d148bf3bfcf76f
@@ -36,8 +36,8 @@
en: 42b15ac6321706aa
zh: 4ed0861911abaf5d
/deepseek-ai-dsh-host-webserver/understand-the-implementation/design-concept:
en: e152e20c9add578c
zh: bacb2e9c31dd1a07
en: 728da97b91b797b6
zh: 22541daf0223c663
/deepseek-ai-dsh-host-webserver/understand-the-implementation/matching-and-lifecycle:
en: 8589aba0cbaa1c76
zh: babf3b27b0cdb35e
+2 -2
View File
@@ -36,7 +36,7 @@ Compose the webserver as the HTTP transport of a browser-facing host, then let t
port: 3000
```
`host` takes one concrete IPv4 or IPv6 literal of a local interface — for example a container's own Pod address from `hostname -i`. A loopback literal (any address in 127/8, `::1`, or a mapped form of either) keeps the server on this machine; any other literal serves the network that address belongs to, over plain HTTP unless `tls` is set. An unspecified address — including the IPv4-mapped forms Linux reads as IPv4 any — is rejected at load instead of opening the port on every interface at once. `port` 0 requests an OS-assigned port; `ctx.webServer.port` reads the listening port afterwards, and the exported `isLoopbackHost(host)` and `isWildcardHost(host)` classify an address by its parsed value, so a mapped or zoned spelling classifies as the address it names. A dotted-quad IPv6 tail names the address's own low 32 bits, exactly as `listen` reads it, so `::0.0.0.1` is loopback `::1` while `::127.0.0.1` is the non-loopback `::7f00:1`; `normalizeBindAddress(host)` returns that address text — `127.0.0.1` for a genuinely mapped `::ffff:127.0.0.1`, `::1` for `::0.0.0.1` and for `::1%lo` — ready to be a URL host. `bindUrlHost(host)` writes that text as a URL host, bracketing IPv6, and returns `undefined` for a non-loopback zoned address that no URL can carry; `ctx.webServer.origin` combines it with the protocol and listening port into the root URL local clients reach, and throws before the port is bound or when no URL form exists.
`host` takes one concrete IPv4 or IPv6 literal of a local interface — for example a container's own Pod address from `hostname -i`. A loopback literal (any address in 127/8, `::1`, or a mapped form of either) keeps the server on this machine; any other literal serves the network that address belongs to, over plain HTTP unless `tls` is set. An unspecified address — including the IPv4-mapped forms Linux reads as IPv4 any — is rejected at load instead of opening the port on every interface at once. `port` 0 requests an OS-assigned port; `ctx.webServer.port` reads the listening port afterwards, and the exported `isLoopbackHost(host)` and `isWildcardHost(host)` classify an address by its parsed value, so a mapped or zoned spelling classifies as the address it names. A dotted-quad IPv6 tail names the address's own low 32 bits, exactly as `listen` reads it, so `::0.0.0.1` is loopback `::1` while `::127.0.0.1` is the non-loopback `::7f00:1`; `normalizeBindAddress(host)` returns that address text — `127.0.0.1` for a genuinely mapped `::ffff:127.0.0.1`, `::1` for `::0.0.0.1` and for `::1%lo` — ready to be a URL host. `bindUrlHost(host)` writes that text as a URL host, bracketing IPv6, and returns `undefined` for a non-loopback zoned address that no URL can carry; `ctx.webServer.getOrigin()` combines it with the protocol and listening port into the root URL local clients reach, and throws before the port is bound or when no URL form exists.
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 or `multipart/form-data`; 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.
@@ -80,7 +80,7 @@ A listen failure (for example EADDRINUSE) rejects plugin initialization with the
### Design concept
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`, `port`, `protocol`, and `origin` getters expose composition-time facts other plugins adapt to (for example the directory-picker chooser), and an explicit `tls: null` normalizes to an unset listener, so `protocol` has a single source.
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`, `port`, and `protocol` getters and `getOrigin()` expose composition-time facts other plugins adapt to (for example the directory-picker chooser), and an explicit `tls: null` normalizes to an unset listener, so `protocol` has a single source.
### Matching and lifecycle
+2 -2
View File
@@ -36,7 +36,7 @@ kind: "package-reference"
port: 3000
```
`host` 接受本机某个接口的一个具体 IPv4 或 IPv6 字面量——例如容器自身来自 `hostname -i` 的 Pod 地址。回环字面量(127/8 内的任意地址、`::1`,或二者的 mapped 形式)把服务器留在本机;其他任何字面量都会在该地址所属网络上提供 HTTP,除非设置 `tls`,否则为明文。未指定地址——包括 Linux 视为 IPv4 any 的 IPv4-mapped 形式——会在加载时被拒绝,而不是一次性打开每个网络接口的端口。`port` 为 0 时请求 OS 分配端口;之后用 `ctx.webServer.port` 读取正在监听的端口,导出的 `isLoopbackHost(host)` 与 `isWildcardHost(host)` 按解析后的地址值分类,因此 mapped 或带 zone 的写法会按其指名的地址分类。点分四段式的 IPv6 尾部表示该地址自身的低 32 位,与 `listen` 的读法完全一致:`::0.0.0.1` 是回环 `::1`,而 `::127.0.0.1` 是非回环的 `::7f00:1`;`normalizeBindAddress(host)` 返回该地址的文本——真正 mapped 的 `::ffff:127.0.0.1` 为 `127.0.0.1`,`::0.0.0.1` 与 `::1%lo` 为 `::1`——可直接用作 URL host。`bindUrlHost(host)` 把该文本写成 URL host(IPv6 加方括号),对 URL 无法携带的非回环带 zone 地址返回 `undefined`;`ctx.webServer.origin` 把它与协议、监听端口组合成本机客户端访问的根 URL,在端口绑定之前或没有 URL 形式时抛错。
`host` 接受本机某个接口的一个具体 IPv4 或 IPv6 字面量——例如容器自身来自 `hostname -i` 的 Pod 地址。回环字面量(127/8 内的任意地址、`::1`,或二者的 mapped 形式)把服务器留在本机;其他任何字面量都会在该地址所属网络上提供 HTTP,除非设置 `tls`,否则为明文。未指定地址——包括 Linux 视为 IPv4 any 的 IPv4-mapped 形式——会在加载时被拒绝,而不是一次性打开每个网络接口的端口。`port` 为 0 时请求 OS 分配端口;之后用 `ctx.webServer.port` 读取正在监听的端口,导出的 `isLoopbackHost(host)` 与 `isWildcardHost(host)` 按解析后的地址值分类,因此 mapped 或带 zone 的写法会按其指名的地址分类。点分四段式的 IPv6 尾部表示该地址自身的低 32 位,与 `listen` 的读法完全一致:`::0.0.0.1` 是回环 `::1`,而 `::127.0.0.1` 是非回环的 `::7f00:1`;`normalizeBindAddress(host)` 返回该地址的文本——真正 mapped 的 `::ffff:127.0.0.1` 为 `127.0.0.1`,`::0.0.0.1` 与 `::1%lo` 为 `::1`——可直接用作 URL host。`bindUrlHost(host)` 把该文本写成 URL host(IPv6 加方括号),对 URL 无法携带的非回环带 zone 地址返回 `undefined`;`ctx.webServer.getOrigin()` 把它与协议、监听端口组合成本机客户端访问的根 URL,在端口绑定之前或没有 URL 形式时抛错。
设置 `compression: 'gzip'` 可以包装符合条件的 socket-backed 响应,而不改变 route API。客户端必须接受 gzip,且媒体类型必须可压缩或为 `multipart/form-data`;已知长度小于 `compressionThresholdBytes` 的响应保持未压缩,未知长度的流则立即符合条件。已有编码、`Cache-Control: no-transform`、range 响应、SSE(Server-Sent Events)、ZIP 与已打包的 `.gz` Worker image 均保持不变。随附 Web bundle 使用 level 1 与 1024 字节阈值;其他组合默认不压缩。
@@ -80,7 +80,7 @@ index 启动输入分两层。`collectIndexInjections()` 收集一张全新的
### 设计理念
本包是一个不带任何 harness 词汇的普通路由注册表:`WebServer` 继承 Cordis `Service`,持有三张路由表、回退 slot、原始 index 转换列表,以及 `webserver/index-inject` 事件,index 渲染器经其收集行。index 渲染每次响应组合两层:`renderIndex` 先把包含提示性 `script-preload` 行的全新注入表渲染进正文,再按注册顺序应用原始转换;`applyIndexTaps` 只运行转换。upgrade handler 拥有协议握手与连接内容;webserver 只交付原始 socket 与 request。`host`、`port`、`protocol` 与 `origin` getter 暴露其他插件据以自适应的组合期事实(例如 directory-picker 选择器),而显式的 `tls: null` 会归一为未设置监听,使 `protocol` 只有一个来源。
本包是一个不带任何 harness 词汇的普通路由注册表:`WebServer` 继承 Cordis `Service`,持有三张路由表、回退 slot、原始 index 转换列表,以及 `webserver/index-inject` 事件,index 渲染器经其收集行。index 渲染每次响应组合两层:`renderIndex` 先把包含提示性 `script-preload` 行的全新注入表渲染进正文,再按注册顺序应用原始转换;`applyIndexTaps` 只运行转换。upgrade handler 拥有协议握手与连接内容;webserver 只交付原始 socket 与 request。`host`、`port`、`protocol` getter 与 `getOrigin()` 暴露其他插件据以自适应的组合期事实(例如 directory-picker 选择器),而显式的 `tls: null` 会归一为未设置监听,使 `protocol` 只有一个来源。
### 匹配与生命周期
+6 -5
View File
@@ -311,7 +311,7 @@ export class WebServer extends Service {
private readonly indexTaps: ((html: string) => string)[] = []
private fallback: WebRoute['handler'] | undefined
private server!: Server
private listenedPort!: number
private listenedPort: number | undefined
private readonly gzip: NodeMiddleware | undefined
private readonly tlsConfig: TlsConfig | undefined
@@ -325,7 +325,7 @@ export class WebServer extends Service {
/** The listening port (the OS-assigned value when config.port is 0). */
get port(): number {
return this.listenedPort
return this.listenedPort as number
}
/** The configured bind address (one concrete local interface). */
@@ -345,14 +345,15 @@ export class WebServer extends Service {
/**
* The root URL local clients use to reach the bound listener: the protocol,
* the {@link bindUrlHost} form of the bind address, and the listening port.
* Read it after `listen` binds, like {@link port}.
* @throws when the bind address has no URL form.
* @returns the origin, for example `http://127.0.0.1:3080`.
* @throws before `listen` binds a port, or when the bind address has no URL form.
*/
get origin(): string {
getOrigin(): string {
const host = bindUrlHost(this.config.host)
if (host === undefined) {
throw new Error(`webserver: bind address ${JSON.stringify(this.config.host)} carries an interface zone id, which no URL can express`)
}
if (this.listenedPort === undefined) throw new Error('webserver: origin read before the listener bound a port')
return `${this.protocol}//${host}:${String(this.listenedPort)}`
}
@@ -295,8 +295,10 @@ describe('real Loader composition', () => {
for (const [host, text] of rows) expect(bindUrlHost(host)).toBe(text)
})
it('reports no origin for a bind address that no URL can express', () => {
expect(() => new HttpServer(new Context(), HttpServer.Config({ host: 'fe80::1%eth0', port: 0 })).origin)
it('reports no origin before binding or for a bind address that no URL can express', () => {
expect(() => new HttpServer(new Context(), HttpServer.Config({ host: '127.0.0.1', port: 0 })).getOrigin())
.toThrow('webserver: origin read before the listener bound a port')
expect(() => new HttpServer(new Context(), HttpServer.Config({ host: 'fe80::1%eth0', port: 0 })).getOrigin())
.toThrow('webserver: bind address "fe80::1%eth0" carries an interface zone id, which no URL can express')
})
@@ -574,7 +576,7 @@ describe('real Loader composition', () => {
const loaded = await loadComposition(0, false, '127.0.0.1', tls)
const server = loaded.webServer
expect(server.protocol).toBe('https:')
expect(server.origin).toBe(`https://127.0.0.1:${String(server.port)}`)
expect(server.getOrigin()).toBe(`https://127.0.0.1:${String(server.port)}`)
server.register({
kind: 'exact',
path: '/tls',
@@ -600,7 +602,7 @@ describe('real Loader composition', () => {
const loaded = await loadComposition(0, false, '127.0.0.1', null)
const server = loaded.webServer
expect(server.protocol).toBe('http:')
expect(server.origin).toBe(`http://127.0.0.1:${String(server.port)}`)
expect(server.getOrigin()).toBe(`http://127.0.0.1:${String(server.port)}`)
server.register({ kind: 'exact', path: '/plain', handler: (_req, res) => { res.writeHead(200); res.end('PLAIN') } })
expect(await request(server.port, '/plain')).toMatchObject({ status: 200, body: 'PLAIN' })
await expect(tlsRequest(server.port, '/plain')).rejects.toThrow()
@@ -51,7 +51,7 @@ Every package below exports a Cordis plugin that a bundle patch can name in a Lo
| `@deepseek-ai/dsh-acp-app` | no | The dsh ACP profile bundle: automation-only JSON-RPC stdio and process lifecycle over dsh-base |
| `@deepseek-ai/dsh-headless` | yes | The dsh one-shot bundle: a direct core Agent/Session runner over dsh-base with no Host, HTTP, or browser layer |
| `@deepseek-ai/dsh-sdk-app` | yes | The dsh SDK profile bundle: stdio JSON-RPC serving and process lifecycle over dsh-base |
| `@deepseek-ai/dsh-web-app` | yes | The dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line) |
| `@deepseek-ai/dsh-web-app` | yes | The dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, bash runtime variables, URL line) |
## client
-3
View File
@@ -2916,9 +2916,6 @@ importers:
'@deepseek-ai/dsh-shell-env':
specifier: workspace:*
version: link:../../shell/shell-env
'@deepseek-ai/dsh-system-prompt':
specifier: workspace:*
version: link:../../core/system-prompt
'@deepseek-ai/dsh-typert-registry':
specifier: workspace:*
version: link:../../typert/registry