跳到主要内容
路径文档

Web UI 架构

审计基线 0.1.5-alpha.1 @ 5dda764ed3;见 源码 / npm 渠道。

一句话版:Web UI 是"宿主进程 + 浏览器端"双进程架构:宿主(host/)持有 agent 与能力,浏览器端(client/)是 React 插件外壳,经连接层(client-connection)通信;client 插件是"dual-face"包的浏览器半部,靠 HMR 热更新。

一、双进程架构​

为什么双进程:能力(agent loop、工具、沙箱)在 host 里跑,UI 在浏览器里渲染。浏览器不持有任何真实能力,只通过连接层订阅 event 流。

动态 Cordis 插件同样遵守双面边界:tool-cordis 把不可变 Package 交给 Host runner;Host 半部在宿主运行,Client 半部经浏览器授权后由 Client runner 异步启动,client-ui-cordis 负责会话卡片。完整生命周期见 运行时自省与动态插件。

二、连接层与浏览器鉴权​

client-connection 持有传输、共享 /api handler 与浏览器会话。只有根路径 GET / 用启动 URL 中的 token 换取 cookie;RPC / WebSocket 即使来自 loopback 也检查它。首次登录应打开 CLI 打印的完整链接,而非只打开干净根地址。

当前源码启动拒绝 --host 0.0.0.0。远程使用 SSH 转发,启动链接应保密。trustedHosts 管允许的 Host authority,不替代登录。

三、Typed Remote API 与领域 controller​

host/apiproxy 已移除。api/gateway 分发生成的 Remote 调用与流,api/remotes 装配应用选定的领域贡献。

  • Session 历史、命令、冷读取与 ownership 策略归 api/session-controller。
  • 设置与工作区分别归 api/settings-controller、api/workspace-controller。
  • 工作区内文件读 / 列目录 / Agent 写变更流归 api/workspace-files(0.1.5 新增),浏览器侧再包装成 file 资源协议;详见 工作区文件服务。
  • 只读历史与会恢复 Agent 的显式命令是不同操作。
  • Session 日志导出仍是独立的已鉴权 HTTP 下载面,而非通用网关配置对象。
  • 未知端点没有旧 API Proxy fallback。详见 Remote API 网关。

四、Client 插件(dual-face)​

一个包可以同时提供 host 半部 + client 半部。client 半部是懒 CJS 模块表:host 扫描 Loader 条目组成 window.__DSH_BOOT__ 启动图,伺服 /plugins/<id>/client.js?rev=<rev>,浏览器半部副作用在首 require 时才执行(window.__ModuleLoader__.load({id, factory}))。

{
"exports": {
".": "./lib/index.js", // host 半部
"./client": "./lib/client.js" // 浏览器半部
},
"dsh": {
"client": {
"inject": [ // 依赖的 client 行,写包名而非服务短名
"@deepseek-ai/dsh-api-session-controller",
"@deepseek-ai/dsh-client-ui-renderer"
],
"platform": "web"
}
}
}

浏览器半部注册 UI(如设置面板 section):

export function apply(ctx: ClientContext): void {
ctx.slots.inject('settings.section', () =>
ctx.slots.register({
name: 'settings.section', id: 'my-plugin', order: 100, label: () => '我的插件',
}, PluginPanel))
}

五、slot 体系:三方可扩展 UI​

slot作用
settings.section设置面板分区(插件在此挂自己的设置卡)
settings.*其它设置位
其它命名 slot由 dsh-client-ui-slots 提供,三方插件按需注册

第三方插件不需要硬编码在 Web UI 源码里:通过 slot 挂进对应位置即可。可配置项在 Agent 预设 里聊。

六、浏览器状态与渲染​

包职责
client/store不依赖 React 的 observable store 与稳定 snapshot
api/session-controller 的 client faceSession 传输、生命周期与控制状态
client/ui-sessionReact / Slot 适配、hooks、SessionProvider
client/ui-chat对话渲染、详情、历史图片与滚动状态
client/ui-approval审批 UI 集成
client/resources统一资源模型:dsh-resource://<type>/… 地址 + useResource,协议归属包注册 provider(0.1.5 新增)
client/file-uploadBlob / 精确字节 / ReadableStream 上传并返回不透明 receipt,供后续 prompt 引用(0.1.5 新增)
client/ui-dockkitdocking 布局引擎(split tree + tabbed panes);内部包,导出随时可变(0.1.5 新增)
client/ui-sidebar-right右侧栏:每会话一个 docking surface,持有 ctx.sidebarRight / ctx.sidebarRightTabs 与 tab 域(0.1.5 新增)
client/ui-sidebar-files右栏工作区文件树 tab 类型,经 workspaceFiles 逐层列目录(0.1.5 新增)
client/ui-sidebar-textpreview右栏纯文本查看器,file 资源地址的兜底类型(0.1.5 新增)
client/ui-schedule只读 Schedule 活动提醒目录;出厂 Web 组合挂成 disabled: true(0.1.5 新增)

浏览器端资源模型与右侧栏四个包的完整拆解见 客户端资源与模块;文件服务见 工作区文件服务。

已移除的旧 dsh-client-runtime 导入应迁到相应 owner,别把所有引用机械替换成 client-store:状态原语、Session 生命周期、React 渲染已拆开。

Chat 默认折叠过程内容与 System prompt 行;只有加载窗口内的用量账目完整且有效,才展示该已完成 turn 的精确 token 用量,避免将部分统计当成完整统计。对话正文宽度可调整。持久化 Session 事件、控制状态投影与临时提交回显要分开理解。

七、访问模式入口​

UI 里的 "Full access / 标准模式" 选择 = permission preset 的入口(见 权限),不是沙箱本身。

八、工作区目录选择(directory-picker)​

directory-picker 是能力 seam:ctx.directoryPicker 是其 Service Definition,唯一方法 capability() 返回判别联合,描述"操作员如何选目录"。后端差别在于用户交互,不只是实现:

kind能力用途
nativepick(signal):开一个原生 OS 选择器操作员坐在宿主显示器前
browselist(path?) / createDirectory(path, name):in-app 列目录、建目录够不到 OS 选择器的 remote client

directory-picker-auto 是自适应选择器:boot 时一次性采样(loopback-only bind、非 SSH launch、有可用显示会话;Linux 需 DISPLAY/WAYLAND_DISPLAY + zenity/kdialog 在 PATH),挂载匹配的 dual-face 后端(native 或 browse)作为 in-memory root tree 的真实 Loader entry(不持久化)。任何模糊都落 browse。

两个后端:

  • directory-picker-native:native capability,pick 每次开一个原生选择器、解析绝对路径(取消返 null);平台工具不用 shell(macOS osascript,Linux Zenity→KDialog,Windows 现代 IFileOpenDialog in spawned child);caller abort 终止 native 进程
  • directory-picker-browse:browse capability,一级目录列目录 + 建子目录(走 Node stdlib,自带 per-OS 适配);只列目录、名字排序、symlink-to-directory 跟随、host 拥有 hidden 标记;crumbs 是 root→target 祖先链;createDirectory 非递归、校验名字为单段

browse primitive 失败抛类型化 DirectoryPickerError(directory-unreadable/directory-exists/directory-create-failed),网关 1:1 映射到 wire error code。

挂载状态:auto 默认挂载(Web 组合,id directory-picker);native/browse 是它 boot 时二选一挂载,也可在 overlay 里直接挂其中之一来 pin 交互。

九、工作区注册表(workspace)​

ctx.workspaceRegistry 是工作区实体注册表:durable workspace record、稳定顺序、newest-first candidate session index,经 domain data form 存储。消费者看到 Workspace 接口,实体实现 package-private。

注册表(ctx.workspaceRegistry):

API约定
create(path, title?)fs.realpath 规范化,拒绝不存在/非目录,每个 canonical path 至多一条 record,prepend 到 durable 顺序
get(id) / list() / resolveByPath(path)缓存命中查询;list() 同步按 durable 顺序;resolveByPath 异步(同样 realpath,拒绝 missing 而非创建)
delete(id)只删 Workspace 注册、顺序 entry、session account;目录/文件/live Session/持久化日志不动,那些 Session 变 Ungrouped
insertBefore(id, before?)registry 级显示顺序重排,DOM-insertBefore 式
archiveSession(id) / archivedSessionIdsregistry-global archive set;归档从分组面消失但保留日志与 sessionIds slot

Workspace 实体(由注册表返回):

API约定
setTitle(title)替换显示标题
attachSession(id)校验 header cwd 对 workspace path,prepend 新 id
insertSessionBefore(id, before?)workspace 内的会话手动排序;workspace 顺序不变
detachSession(id)只删 candidate index entry
status()未缓存目录检查,'ok' | 'missing-dir'

挂载状态:默认挂载(Web 组合)。

十、前端静态伺服(host-frontend-static)​

host-frontend-static 是 SPA dist server:function 插件(config {distIndex}),占据 webserver 的唯一 fallback seat,伺服构建好的前端目录,带 shell 的锁定语义——穿越 dist root 之外 403、dist root 与配置的 index 路径渲染 index.html HTTP 200、其余缺失/非文件目标(缺文件、目录、缺 index)返回空 404(不是把所有 miss 回落成 SPA 200)、未知扩展名 application/octet-stream、无匹配命名 route 的 non-GET/HEAD 405。每个 index response 走 webserver 的 renderIndex(内部即 applyIndexTaps),boot manifest 经此到达页面;served index 还注入 <base href="/">,让深层路径下的相对资源仍锚定站点根。

distIndex 是组合应用的 assembly fact:dsh-web-app 经前端包的 exports 解析它并挂载此插件。fallback seat 单 owner(第二个 claim 抛错)、effect-scoped(dispose 释放后未 claim 的 webserver 回 404)。

挂载状态:默认挂载(由 Web 组合的 web-runtime row 挂)。

十一、构建产物​

pnpm run build # 先构建匹配的源码版本
pnpm dsh web # 启动 Web profile
pnpm run build:web # 只构建前端
  • 开发 HMR 在源码目录跑 pnpm run dev:web(重建 lib/client.js),dsh web 的 webserver 轮询到重建自动热更

十二、验证​

# 看 host 进程监听的端口
lsof -nP -iTCP:3080 -sTCP:LISTEN

# 看 client 插件是否被装载
dsh web --dump-config | grep -i client

# 看 API 网关 / 目录选择 / 工作区 / 前端伺服是否装载
dsh web --dump-config | grep -iE "api-gateway|controller|directory-picker|workspace|host-frontend-static"

下一步​