dsh-file-explorer
Workspace file explorer docked in the DeepSeek Harness sidebar — lazy file tree, Settings toggle, drag-to-resize. Also shipped as a single-session Cordis dynamic plugin.
- Stars
- 0
- Language
- JavaScript
- Created
- Sep 10, 2026
- Updated
- Sep 10, 2026
Introduction
dsh-plugin-file-explorer
停靠在 DeepSeek Harness 侧边栏里的工作目录文件浏览器。
English | 中文
它把当前会话工作目录的懒加载目录树放在侧边栏左下角——就在 设置 那一行的正上方——
自带筛选、刷新、设置 → 通用里的开关,以及可拖动的高度边缘。宽度跟随侧边栏列。

面板停靠在侧边栏脚部,位于 Cordis Plugin 与 设置 上方。宽度与侧边栏一致,高度可拖动上边缘调整。
┌──────────────────────────────┐
│ 新会话 │
│ 工作区 ⌕ ⋯ │
│ ▾ DeepseekWorkSpace │
│ 会话一 2m │
│ 会话二 4d │
│ │
│ ──────────────────────────── │ ← 拖动这条边调整高度
│ ▸ 文件浏览器 ↻ │
│ 筛选文件… │
│ ▾ 📁 src │
│ ▸ 📁 client │
│ 📄 index.js │
│ 📄 package.json │
│ ▸ 📁 test │
│ ──────────────────────────── │
│ Cordis Plugin 1 running │
│ ⚙ 设置 │
└──────────────────────────────┘
目录
功能
| 停靠而非浮窗 | 渲染在侧边栏自己的脚部座位上,使用侧边栏的底色——没有卡片、边框、圆角或阴影,看起来就是这一列的一部分。 |
| 宽度跟随侧边栏 | 面板测量座位容器,并用 ResizeObserver 跟踪变化,所以拖动侧边栏边缘时面板始终对齐。 |
| 拖动调整高度 | 上边缘是一条 8px 拖拽热区(ns-resize)。默认高度约为侧边栏的一半,并被限制在脚部分区以上的空间内,标题行永远不会被裁掉。 |
| 懒加载目录树 | 只读取你展开的层级。目录优先排序,展开状态在重渲染后保留,筛选框按名称过滤已加载层级。 |
| 内存态开关 | 设置 → 通用 → 文件浏览器。面板标题行左侧的箭头是同一个开关的快捷方式——它把面板收起到只剩标题行。 |
| 只读且有围栏 | 宿主半只列目录项,拒绝会话工作目录之外的任何路径,每次列举上限 800 项。 |
| 双语 | 通过 ctx.locale 注册英文与简体中文字典,面板跟随界面语言。 |
| 感知收起态 | 侧边栏收起成 56px 竖条时,面板完全不渲染。 |
| 安装无需构建 | lib/ 以构建产物形式提交,git 依赖可以直接用。 |
前置要求
- 带 Web GUI 的 DeepSeek Harness(
dsh web)。开发与验证基于@deepseek-ai/dsh0.1.5-rc.1 及其自带的webprofile。 - Node.js ≥ 20(宿主半使用了全局
URL与Buffer.byteLength)。 - 宿主需要提供
fs与webServer服务——两者都属于标准 Web profile。 - 本插件不使用网络、不需要 API Key、不读取任何凭据。
两种安装方式
| profile 插件 | 动态插件 | |
|---|---|---|
| 形态 | 真实的 npm 风格包,作为 composition 的一行挂载 | 两段纯 JS,通过 Cordis 工具加载 |
| 生命周期 | 跨重启存活,属于你的 profile | 只存在于当前 DSH 进程 |
| 安装 | 一条命令 —— dsh plugin --profile web add …(自动激活) | 让你的 agent 加载 dynamic-plugin/ |
| 是否改动 profile | 是 | 否 |
| 适用 | 日常使用 | 单个会话里先试试 |
作为 profile 插件安装
1. 把包装进 profile
CLI 会把 profile 名之后的参数原样转发给 profile 目录里的 pnpm,而插件的模块解析正是
从这个 profile 目录出发的:
# 直接从 Git 仓库安装(无需发布 npm,lib/ 已是构建产物):
dsh plugin --profile web add github:mabaoguo9527/dsh-file-explorer
# 固定某个发布版本:
dsh plugin --profile web add github:mabaoguo9527/dsh-file-explorer#v0.1.2
# 或者从本地检出安装——开发时用这个:
dsh plugin --profile web add /absolute/path/to/dsh-file-explorer
2. 重启 Web UI
安装就到这里。本包声明了 dsh.bundle,所以 CLI 会自动把它追加到
dsh.profile.bundles(profile 的有序层列表),并把它的
cordis.patch.yml 作为一个层应用:
// $DSH_HOME/profiles/web/package.json,由 `dsh plugin add` 写入
"dsh": {
"profile": {
"bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-plugin-file-explorer"],
"patchReload": "live"
}
}
层列表在启动时读取,所以重启一次 dsh web。想在不启动服务的前提下检查组合结果:
dsh --profile web --dump-config | grep -A2 dsh-plugin-file-explorer
备选:自己挂载 composition 行
如果你不想重启,或者想在正式安装前先评估,同一行也可以作为你自己掌控的 patch 层应用。
仓库已经把它放在 cordis.patch.yml 里:
- insert:
- id: file-explorer
name: 'dsh-plugin-file-explorer'
先不改任何文件试一次,把该文件当作额外的 patch 层传入:
dsh --profile web --patch ./node_modules/dsh-plugin-file-explorer/cordis.patch.yml
不走 bundle 路线时长期生效:把这条 entry 合并进 profile 本来就会加载的 patch 文件
$DSH_HOME/profiles/web/cordis.patch.yml($DSH_HOME 默认是 ~/.dsh)。该文件默认内容
是空数组 [],所以多数情况下直接整体替换;如果里面已经有内容,就把这个 - insert:
条目追加到既有的顶层数组里,而不要新增一个 YAML 文档。
patch 文件是被监听的(自定义 profile 默认 patchReload: live),所以这条路线无需重启:
刷新浏览器页面即可。
两条路线只能选一条。既手动挂载了这一行、又把包留在
dsh.profile.bundles里,会导致同一个 row id 被插入两次。
验证
打开 GUI:面板出现在 设置 行上方,设置 → 通用里会多出一个 文件浏览器 开关。
想不用 UI 确认组合结果:
dsh --profile web --dump-config | grep -A2 dsh-plugin-file-explorer
作为动态插件安装
dynamic-plugin/ 用单会话的 Cordis 动态插件实现了同样的功能:
host.js 与 client.js,都是纯 JavaScript,不需要包、不需要 composition 行、不改
profile。
把这两个文件交给 DSH agent,让它定义并运行插件即可——agent 会用两半代码调用
cordis_define,然后调用 cordis_run。面板立刻出现;DSH 进程重启后消失,因为动态插件
是进程内的。具体提示词与和 profile 插件的差异见
dynamic-plugin/README.md。
使用方式
| 操作 | 结果 |
|---|---|
| 点击目录行 | 展开或收起该层,首次展开时才读取 |
| 点击文件行 | 选中它,底部显示它相对工作目录的路径 |
筛选文件… | 按名称过滤已加载的层级;目录仍可继续展开 |
↻ | 重新读取根目录以及所有已展开的层级 |
| 标题行的箭头 | 把面板收起到只剩标题行,或再次展开 |
| 拖动上边缘 | 调整高度;不会超过侧边栏脚部,也不会低于 120px |
| 设置 → 通用 → 文件浏览器 | 整块开关 |
标题行显示工作目录的末级目录名,悬停可看到绝对路径。
面板在运行中的应用里就是这样一块:
配置
插件不接受任何插件配置。它唯一的偏好就是那个开关,而且该状态刻意保持在内存中:
它存在于插件 fiber 里,插件(重新)加载时重置为开,不会写入 settings.yaml。动态插件
本身就是进程内的,而一个布尔值不值得单开一个 settings 命名空间,所以这个开关是有意做成
会话级的。
高度同理,保存在内存中,重新加载后回到默认高度。
安全
宿主半之所以存在,是因为浏览器读不到 harness 的文件系统。它的全部对外面就是一个只读路由, 限制都是刻意设计的:
- 收容校验:每个请求都会先把会话工作目录解析为根,再对解析后的目标做
fs.contains(root, target)判断,因此逃逸出根的..段与符号链接会被400拒绝, 而不是被渲染出来。 - 只读元数据:只使用
fs.stat与fs.listDir,从不读取文件内容,也没有任何写入、 重命名、移动或删除的接口。 - 有上界:一次列举最多返回 800 项。
- 默认本地:路由由 harness 的 Web 服务器提供,因此继承该服务器的绑定地址与可信主机 策略。如果你把 GUI 绑定到局域网网卡,那么任何能访问 GUI 的人都能列举工作目录内的目录。 除非你确实需要,否则请绑定回环地址。
- 不碰凭据:插件从不访问
ctx.credentials,也不发起任何对外网络请求。
实现原理
一个功能,两半实现。
宿主半 —— lib/index.js。 一个 Cordis 插件(apply(ctx) +
inject = ['webServer', 'fs']),注册一个 exact 路由 /dsh-file-explorer/tree。
处理函数通过组合出来的 fs 服务解析目标目录、执行上面的收容与数量限制,然后返回 JSON。
注册放在 ctx.effect(...) 里,所以卸载插件时会释放该路径,而不是留下悬空的处理函数。
浏览器半 —— lib/client.js。 以客户端模块系统从 exports["./client"] 提供的
构建产物格式发布,也就是交给
window.__ModuleLoader__.load({ id, factory }) 的一个工厂函数。它只请求基线模块
react,这也是 dsh.client.external 为空、完全不需要打包器的原因。它导出的
inject = ['slots', 'locale'] 让 Cordis 在 apply 执行前先等待座位注册表与字典注册表。
它坐在哪里。 两个增量座位,都是 replaceRisk: none:
| 座位 | 用途 | 注册 |
|---|---|---|
sidebar.footer.action | 侧边栏脚部、设置 上方那一行 | id: dsh-file-explorer、order: -100 |
settings.general.item | 设置 → 通用 里的一个偏好行 | id: dsh-file-explorer、order: 30 |
为什么面板用绝对定位而不是普通流。 sidebar.footer.action 是一个横向 flex 行,而
内置占用者(Cordis Plugin 触发器)是 flex: none; width: 100%——它独占整行。于是任何
在流内的兄弟条目都会被压成精确的 0 宽度:高度保留(留下一片空白),但每个子元素都被
挤成只剩自己的内边距并被裁掉。因此这里的条目是一个零尺寸 flex 项,只作为定位锚点,
面板本身脱离该行绝对定位:
left: calc(-1 * var(--dsh-sidebar-inline-padding))贴到侧边栏列的左边缘;- 锚点上的
bottom: 0就是脚部分区的顶部边缘,于是面板向上生长,正好落在内置触发器 上方,不需要任何写死的偏移; - 该列的
overflow: hidden会把面板裁在侧边栏内,因此它永远不会溢到会话栏。
宽度。 侧边栏不向座位条目暴露宽度——既没有对应的 slot prop,也没有 CSS 变量。因此面板
从自己的节点向外找第一个有真实盒子的祖先(座位容器),在其宽度上左右各补一次继承来的
--dsh-sidebar-inline-padding,并用 ResizeObserver 跟踪变化。以上任何一步失败时,都会
回退到 CSS 里的固定宽度,面板照常渲染。
高度。 上边缘使用 pointer capture 拖拽。拖拽的起始状态放在插件闭包里而不是组件内, 因为 React 每次渲染都会重建组件内的绑定,否则拖到一半的拖拽会被重置。
HTTP 接口
浏览器半使用这个路由;它足够稳定,可以直接脚本化调用。
GET /dsh-file-explorer/tree?base=<绝对路径>&path=<相对 base 的路径>
base 是会话工作目录(. 表示回退到文件系统后端自己的默认值)。path 相对 base;
. 表示 base 本身。
// 200
{
"path": "/Users/you/project/src", // 解析后的绝对目录
"entries": [
{ "name": "client", "type": "directory", "size": null },
{ "name": "package.json", "type": "file", "size": 812 }
]
}
type 取值 file、directory、other。后端不上报大小时 size 为 null。错误返回
{ "error": "<消息>" },状态码为 400(超出工作目录、不是目录)、404(不存在)、
405(方法不允许)或 500(后端失败)。
兼容性
- 基于
@deepseek-ai/dsh0.1.5-rc.1(Web profile)构建与验证。 - 只使用有文档的接缝:
fs与webServer服务、slots与locale客户端服务、sidebar.footer.action与settings.general.item座位、dsh.client包声明,以及 用于清理的ctx.effect。 - 唯一的结构性假设是上面描述的宽度测量。它写得比较防御,失败时退化为固定宽度而不是报错; 但如果未来版本改变了侧边栏的 DOM 嵌套,请预期退化为回退宽度。
- 主题颜色都取自已有的 Design Token 变量(
--dsw-specific-sidebar-fill、--dsw-alias-*), 因此浅色/深色主题自动跟随,无需额外处理。
疑难排查
面板不见了。
按顺序检查:composition 行是否存在(dsh --profile web --dump-config);侧边栏是否被收起成
56px 竖条;设置 → 通用 → 文件浏览器 开关是否为开(重新加载插件会把它恢复为开)。
面板在,但是空的并带一行错误。
错误文本就是宿主半原样返回的内容。path is outside the working directory 通常意味着会话的
工作目录在面板打开期间变了——按 ↻。path not found 意味着该目录被移动或删除了。
重启 dsh web 后面板消失了。
动态插件方式下这是预期行为:动态插件是进程内的。想跨重启保留,请按 profile 插件方式安装。
主题看起来不对。 面板使用与侧边栏本身相同的表面色与文本色 token。自定义主题若重定义了它们,面板会跟随;如果 发现某个 token 缺失,请提 issue。
拖动时感觉卡住。 拖拽使用 pointer capture,所以拖到面板外也会继续。若浏览器丢掉了 capture,松手重新拖即可; 高度在每次 move 时都已提交。
卸载
# 1. 从 $DSH_HOME/profiles/web/cordis.patch.yml 中删掉该 `- insert:` 条目(或整块)
# 2. 移除包
dsh plugin --profile web remove dsh-plugin-file-explorer
# 3. 若 profile 使用 patchReload: startup,重启 dsh web
动态插件方式则在侧边栏 设置 上方的 Cordis 面板里停止或移除该插件。
开发
dsh-file-explorer/
├── lib/
│ ├── index.js # 宿主半 —— 目录列举路由
│ └── client.js # 浏览器半 —— 停靠面板与设置行
├── cordis.patch.yml # 挂载插件的 composition 行
├── dynamic-plugin/ # 同一功能的单会话动态插件版
├── test/ # node:test 冒烟测试(不需要浏览器)
└── .github/workflows/ # CI:语法检查 + 测试
没有构建步骤:lib/client.js 本身就是 bundle,直接按客户端模块系统提供的格式编写。
检查命令:
npm run check # 对两半执行 node --check
npm test # node:test 冒烟测试
验证状态
-
动态插件版本已在
0.1.5-rc.1的真实 Web GUI 中端到端使用过:加载目录、展开、 筛选、设置开关、宽度跟随与拖动调整高度。 -
profile 插件包由
npm test覆盖——宿主半针对真实临时目录运行(列举、越界拒绝、404、405),浏览器 bundle 会被实际求值并断言其两处注册;另外还把package.json与内置dsh.client解析器的规则做了交叉核对(dsh-client-modules:platform必须是 字符串、Loader row 名必须是裸包名、exports["./client"]必须是字符串或{ default: string }、 不得声明external请求)。 -
profile 插件包已在隔离实例(独立
DSH_HOME)上、对本包进入dsh.profile.bundles的真实服务端做了端到端验证:组合树里有file-explorer行; 路由返回200与正确目录列表(越界路径400、不存在404、非 GET405); 浏览器 bundle 由/plugins/??dsh-plugin-file-explorer/client.js正常提供。 要对活的 GUI 迭代开发,把本地检出作为本地依赖安装 (dsh plugin --profile web add /absolute/path/to/dsh-file-explorer),启动命令里保留--patch,改完lib/client.js后刷新页面即可。
参与贡献
欢迎提 issue 与 PR。请保持本插件赖以成立的两条不变量:浏览器半除了基线 react 模块之外
不得依赖任何东西;宿主半必须严格只读,并且被限制在工作目录内。提交 PR 前请先跑
npm run check && npm test。
如果你愿意为这个 README 补一张真实会话里的面板截图,非常欢迎。
许可证
MIT © 2026 mabaoguo9527
致谢
- DeepSeek Harness 以及本插件所接入的 Cordis 插件框架。
- 插件遵循 harness 自身的约定——
apply(ctx)模块、基于座位的 UI 注册、ctx.locale字典、ctx.effect清理——参见官方教程 Your first plugin。 - 面板样式对齐内置工作区浏览器(
ui-workspace)的度量:36px 分区标题行、28px 圆形图标按钮、 28px 行高与 8px 圆角。