Files
deepseek-harness/packages/host/frontend-static/README.zh.md
T
imccyu 73931626be feat(webserver): add resource routes and in-process readResource (#6901)
* feat(webserver): add resource routes and in-process readResource

A webserver route or the fallback seat may now answer through a resource,
a function from a Fetch Request to a Response, besides the existing
node:http handler. register accepts either route form and registerFallback
accepts a handler or { resource }; existing handler callers are unchanged.
HTTP GET and HEAD requests reach a resource; webServer.readResource(request,
peer) follows the same match order and reaches only resources, so handler
routes stay unreachable to in-process readers.

client-modules registers /plugins as the fetchBundle resource, and
frontend-static claims the fallback seat with a resource that renders the
index for a supplied Peer, or runs Connection's existing authorizeIndex for
an HTTP request and renders for the browser Peer.

* fix(webserver): flush resource headers and keep one drain listener per response

Resource responses send their status and headers before the first body
chunk, register one drain/close listener pair per response so the gzip
middleware's zlib stream does not accumulate listeners across backpressure
waits, and readResource drops the body of a HEAD read as HTTP does. HTTP
requests and readResource share one responder lookup.

* chore(scripts): prune the retired client-modules unknown-cast baseline entry

* feat(frontend-static): let browsers revalidate dist files except the index

Every dist file other than the rendered index now carries
cache-control: no-cache and a weak ETag from a SHA-1 of its bytes; a
request whose If-None-Match equals that ETag receives an empty 304. The
index keeps sending no cache headers. In-process reads through
webServer.readResource revalidate the same way.

* fix(webserver): stop reading Host for resource URLs and copy no held body

A resource request's URL is the request target resolved against the same
placeholder origin the route match uses, so a missing or unparsable Host no
longer turns a dist or /plugins request into 400. The webserver ends a
resource response itself and treats any earlier close as an abort, which
also covers the Web Worker tunnel marking an aborted response ended first.
client-modules bundle bodies and frontend-static file bodies are the held
bytes themselves instead of copies, and frontend-static no longer exports
its internal serveStatic helper.

* docs(webserver): document resource route types and the handler-or-resource fallback

Paste WebResource and WebResourceRoute beside WebRoute on the web-server
subsystem page and link the catalog signatures to it, replace the remaining
'fallback handler' wording now that the seat takes a handler or a resource,
and note why the drain listener removal is a no-op under gzip.
2026-10-22 13:41:18 +08:00

7.2 KiB
Raw Blame History

description, kind
description kind
Web 壳的 SPA dist 服务器:占据 webserver 回退席位,以遍历拒绝与 SPA index 回退服务已构建的前端。 package-reference

@deepseek-ai/dsh-host-frontend-static

English | 中文

概述

从配置的发布目录向浏览器提供已构建的 Web 壳。根路径与配置的 index 路径渲染包含启动信息的 index;已有资产直接提供,而缺失或非文件路径返回 404、路径遍历返回 403、不支持的方法返回 405。访问 index 需要有效的进程 token 或浏览器 cookie,但静态资产仍可公开访问。同一时间只能有一个实例处理未匹配的路由;第二个实例启动失败,卸载活动实例后,未匹配的请求返回 404。

目录


使用本包

在服务已构建 Web 壳的浏览器宿主中组合本插件:它占据 webserver 的回退席位,并应答所有未被具名路由命中的请求。它只需要一个配置值——已构建前端的 index.html 位于何处。

最小配置

- name: '@deepseek-ai/dsh-host-frontend-static'
  config:
    distIndex: /absolute/path/to/dist/index.html

distIndex 是组合应用的组装事实:dsh-web-app 通过前端包的 exports 解析它并挂载本插件;部署绝不硬编码它。

服务器实施的约束

请求从 dist 根目录(包含 distIndex 的目录)提供。dist 根目录与配置的 index 路径以 HTTP 200 渲染 index.html;任何其他已有文件按自身 MIME 类型直接提供,未知扩展名按 application/octet-stream 提供。解析到根目录之外的路径以 403 拒绝,因此精心构造的路径无法读取 dist 之上的文件。dist 根目录内不存在或不是文件的目标——文件缺失、目录或配置的 index 缺失——返回空 404。没有匹配具名路由的非 GET/HEAD 请求返回 405。回退是一个 webserver resource。每个成功的 index 响应都经 webserver 的 renderIndex 渲染——经 HTTP 时为 connection.browserPeer(Connection 浏览器认证所准入的 Peer),在 webServer.readResource(request, peer) 读取中为 peer 且跳过浏览器认证——因此启动 manifest(元数据清单)与该 Peer 的访问快照会通过 / 与配置的 index 路径送达页面。

所服务的 HTML 携带唯一的文档 base <base href="./">,位于每一条注入资源行之前,因此它冻结页面加载时所处的入口目录:shell 自身的应用目录相对引用与宿主的插件资源行都在服务该页面的挂载下解析。同一份 index 因而既服务源站根目录,也服务剥离前缀的代理所拥有的任一挂载;本插件只为 dist 根目录与配置的 index 路径渲染它。

对根路径与配置 index 的 HTTP 请求会在读取 HTML 前调用 ctx.connection.authorizeIndex。有效进程 token 会得到 303 重定向与持久浏览器 cookie;已有有效 cookie 时直接提供 index;其他 index 请求得到 Connection 所有的 401 响应。非 index 文件仍是公开静态资源。Token、cookie、过期时间与签名记录语义都归 Connection 所有。

除 index 外,所有提供的文件都由浏览器重新验证。这类响应携带 cache-control: no-cache 和由文件内容 SHA-1 生成的弱 ETag,因此文件内容变化后即使名字不变也会得到新的 ETag;If-None-Match 等于当前 ETag 的请求得到空的 304。渲染出的 index 不带缓存头。

可观察的失败

遍历返回 403 而不是错误页。dist 根目录内不存在或不是文件的目标返回空 404,因此失效链接或拼错的 pathname 是显式失败,而不是静默的 SPA 回退。第二次占据席位会抛错,而席位无人占据时 webserver 返回 404——本插件的 fiber 被 dispose(资源释放)后,浏览器看到的就是该响应。


理解实现

实现细节——点击展开

设计理念

本包是围绕 serveStatic 的一个函数插件,serveStatic 把 Fetch Request 映射为 Response:apply 从 distIndex 解析出 dist 根目录,并在 effect 作用域下注册回退 resource。每个请求中,resource 交给 serveStatic 一个 authorizeIndex 闭包——只在请求没有携带 Peer 时运行 ctx.connection.authorizeIndex——以及一个 renderIndex 闭包:它为该 Peer 或浏览器 Peer 对原始 index.html 运行 ctx.webServer.renderIndex,并把文档 base 拼接在起始 head 标签之后(在原始转换之后执行,因此也先于其标记)。按 webserver 的约定,席位只有单一所有者——第二次注册会抛错——且受 effect 作用域约束,因此 dispose fiber 即释放席位。

遍历栅栏

serveStatic 规范化请求的 pathname 并拼接到 dist 根目录,然后要求目标就是根目录本身或保持在它之下。检查使用 sep 而非 /,因为 resolve() 在 Windows 上输出反斜杠路径,此时 / 后缀会把每个合法子路径都当作遍历拒绝。

源码地图

文件 职责
src/index.ts serveStatic 与 apply:回退 resource、遍历拒绝、index 鉴权与渲染、重新验证、MIME 表

进一步探索

当服务约定不够用时阅读以下内容:先看席位所有者的约定,再看解析 dist 的组合与子系统参考。


模型体验

无。该 SPA dist 服务器只应答浏览器资产请求,不注册任何面向模型的内容。

KV Cache 影响

无;该包既不组装也不发送提供方请求。

已知限制与延期工作

这些限制说明某个资产类别何时尚未被覆盖。它们是当前包约束,不是任务积压。

  • 初始 MIME 表很精简:它覆盖 Vite 输出的资产集合及实际交付的 PWA manifest;其他扩展名在相应资产类别发布前都会回退到 application/octet-stream。
  • Pathname 路由是显式声明——当前客户端从根目录或配置的 index 路径进入,没有 History API pathname 路由。新增一条需要显式服务器规则与真实组合覆盖,而不是对每次未命中做宽泛回退。

开发备注

维护者的工作上下文——点击展开

无。