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 face | Session 传输、生命周期与控制状态 |
client/ui-session | React / Slot 适配、hooks、SessionProvider |
client/ui-chat | 对话渲染、详情、历史图片与滚动状态 |
client/ui-approval | 审批 UI 集成 |
client/resources | 统一资源模型:dsh-resource://<type>/… 地址 + useResource,协议归属包注册 provider(0.1.5 新增) |
client/file-upload | Blob / 精确字节 / ReadableStream 上传并返回不透明 receipt,供后续 prompt 引用(0.1.5 新增) |
client/ui-dockkit | docking 布局引擎(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 | 能力 | 用途 |
|---|---|---|
native | pick(signal):开一个原生 OS 选择器 | 操作员坐在宿主显示器前 |
browse | list(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:nativecapability,pick每次开一个原生选择器、解析绝对路径(取消返null);平台工具不用 shell(macOSosascript,Linux Zenity→KDialog,Windows 现代IFileOpenDialogin spawned child);caller abort 终止 native 进程directory-picker-browse:browsecapability,一级目录列目录 + 建子目录(走 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) / archivedSessionIds | registry-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"