🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

文件工作台 ghbhiee/dsh-plugin-workbench

DeepSeek Harness
ghbhiee75 天前有提交已提供中文文档扫描:含敏感能力

在会话侧栏浏览文件、预览内容并打开终端

24.6/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/14 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add ghbhiee/dsh-plugin-workbench
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:待复验
是什么
dsh 原生插件 · browser
装得上吗
曾在 dsh 0.2.0-rc.2 下真实安装成功,但当前 dsh 已更新至 0.2.1-rc.5,本站尚未在新版下复验
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
更新放缓:最近一次提交在 75 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

读写/删除本地文件发起外部网络请求
查看扫描证据(共 30 处,展示前 14 条)
  • 读取本地文件
    lib/index.js:65
    if (!statSync(path).isDirectory()) problems.push(`${path} — not a directory`);
  • 读取本地文件
    lib/index.js:114
    canonical = await realpath(candidate);
  • 读取本地文件
    lib/index.js:121
    canonicalRoot = await realpath(root.path);
  • 读取本地文件
    lib/index.js:149
    canonicalRoot = await realpath(root.path);
  • 读取本地文件
    lib/index.js:154
    canonicalTarget = await realpath(absolutePath);
  • 读取本地文件
    lib/index.js:158
    canonicalTarget = `${await realpath(dirname(absolutePath))}/${basename(absolutePath)}`;
  • 写入本地文件
    lib/index.js:682
    } else await writeFile(absolutePath, body);
  • 写入本地文件
    lib/index.js:696
    await mkdir(absolutePath, { recursive: true });
  • 写入本地文件
    lib/index.js:709
    await rename(absolutePath, destination.absolutePath);
  • 删除本地文件
    lib/index.js:721
    await rm(absolutePath, {
  • 写入本地文件
    lib/index.js:768
    chmodSync(helper, 493);
  • 启用终端会话(PTY)
    lib/index.js:872
    const child = pty.spawn(file, args, {
  • 发起网络请求
    lib/client.js:256
    const response = await fetch(`${BASE}${path}?${query}`, { headers: { accept: "application/json" } });
  • 发起网络请求
    lib/client.js:324
    const response = await fetch(`${BASE}${path}?${query}`, init);
扫描于 2026/10/31(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/schemastery@deepseek-ai/cordis@deepseek-ai/dsh-agent@deepseek-ai/dsh-client-locale@deepseek-ai/dsh-client-runtime@deepseek-ai/dsh-client-ui-conversation@deepseek-ai/dsh-client-ui-layout@deepseek-ai/dsh-client-ui-primitives@deepseek-ai/dsh-client-ui-sidebar@deepseek-ai/dsh-client-ui-slots@deepseek-ai/dsh-client-ui-theme@deepseek-ai/dsh-client-web-react
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

文件浏览、预览与 Web 终端面板,DeepSeek Harness 的会话停靠工作台插件

README

由 DeepSeek 最新模型翻译生成
dsh-plugin-workbench 为 DeepSeek Harness web UI 提供的文件浏览器、文件预览和浏览器终端,以 out-of-tree 插件形式实现。 安装 还没上 npm。构建产物 lib/ 已提交到仓库,所以可以直接从 GitHub 安装——不用下载,不用构建: dsh plugin --profile web add github:ghbhiee/dsh-plugin-workbench 或者从本地克隆安装(用 link:,本地重新构建后能直接生效): git clone https://github.com/ghbhiee/dsh-plugin-workbench.git dsh plugin --profile web add ./dsh-plugin-workbench 同系列插件安装方式相同:dsh-plugin-workbench、 dsh-plugin-mobile-shell、 dsh-plugin-cli-session。 开发 pnpm install pnpm run check # typecheck → vitest → tsdown build 因为 lib/ 是纳入版本管理的(git 安装取的就是它),每次改源码都要重新构建并提交。 配置 所有会改动文件或启动进程的功能默认关闭。
展开完整文档(README 剩余 1 节)
dsh-plugin-workbench

为 DeepSeek Harness web UI 提供的文件浏览器、文件预览和浏览器终端,以 out-of-tree 插件形式实现。

安装

还没上 npm。构建产物 lib/ 已提交到仓库,所以可以直接从 GitHub 安装——不用下载,不用构建:

dsh plugin --profile web add github:ghbhiee/dsh-plugin-workbench

或者从本地克隆安装(用 link:,本地重新构建后能直接生效):

git clone https://github.com/ghbhiee/dsh-plugin-workbench.git
dsh plugin --profile web add ./dsh-plugin-workbench

同系列插件安装方式相同:dsh-plugin-workbench、 dsh-plugin-mobile-shell、 dsh-plugin-cli-session。

开发

pnpm install
pnpm run check # typecheck → vitest → tsdown build

因为 lib/ 是纳入版本管理的(git 安装取的就是它),每次改源码都要重新构建并提交。

配置

所有会改动文件或启动进程的功能默认关闭。在配置档的 cordis.patch.yml 里覆盖:

- id: workbench
config:
ptyEnabled: true
readRoots:
- /Users/me/notes

| 字段 | 默认值 | 含义 |
| readRoots | [] | 除会话工作区根目录外,浏览器可读取的额外绝对目录。加载时校验:相对路径或目录不存在都会让插件加载失败,并同时报出这两个问题,而不是在文件选择器里留下一个坏条目 |
| writeEnabled | false | 允许写入/上传/mkdir/重命名/删除,并显示编辑界面 |
| ptyEnabled | false | 允许浏览器启动 shell |
| loopbackOnly | true | 拒绝不是通过 loopback 到达的调用方 |
| maxListEntries | 1000 | 单次目录列表的条目数上限 |
| shell | '' | 要启动的 shell;留空则依次探测 $SHELL → zsh → bash(Windows 上是 PowerShell) |

接口

| 路由 | 用途 |
| GET /plugins/workbench/api/roots | 可读根目录 |
| GET /plugins/workbench/api/list?root=&path= | 目录列表,含类型、大小、mtime,以及符号链接指向哪里 |
| GET /plugins/workbench/api/search?root=&path=&q= | 在目录下按文件名搜索,受结果数/条目数/时间限制 |
| GET /plugins/workbench/api/stat?root=&path= | 类型、大小和版本——开销很小,预览打开时可以轮询 |
| GET /plugins/workbench/api/read?root=&path= | 文本内容,外加表示新鲜度的 version(上限 2 MiB,拒绝二进制和非 UTF-8) |
| GET /plugins/workbench/api/bytes?root=&path=[&download=1] | 原始字节加 MIME 类型,用于图片和 PDF;download=1 时作为附件发送,而不是内联 |
| PUT /plugins/workbench/api/write?root=&path=[&version=] | 请求体是新文本;带 version 时写入是条件式的,响应里会带上新版本 |
| POST /plugins/workbench/api/upload?root=&path= | Body 是原始字节 —— 循环里没有 multipart 解析器 |
| POST /plugins/workbench/api/mkdir?root=&path= | 创建目录(递归) |
| POST /plugins/workbench/api/rename?root=&path=&to= | 在同一个 root 内移动 |
| DELETE /plugins/workbench/api/delete?root=&path=[&recursive=1] | 删除;目录需要 recursive=1 |
| WS /plugins/workbench/pty | 终端网关 |

保存让你保持同步。 写入回复会带上文件的新版本号,编辑器可以接着编辑,新鲜度轮询也有东西可对比 —— 没有它,每次保存之后都要白白重读整个文件。

保存不会覆盖别人的改动。 read 会返回 fs seam 的新鲜度 token,编辑器在保存时把它发回去,所以只要写入的依据已经变了,就会被拒绝并返回 409 stale_version,草稿留在 textarea 里,既不会丢失,也不会覆盖别人的改动。上传和无条件写入照旧可用。

不会有任何东西被悄悄替换。 POSIX rename() 会一声不吭地覆盖目标,所以重命名到一个已存在的名字会毁掉它,还回 {ok:true};现在这种情况需要显式传 overwrite=1,否则失败并返回 409 destination_exists。写入和上传本来就是合法替换,所以它们会报告 overwrote,浏览器会说明替换掉了什么。

root 永远不能作为目标。 空的 path 会解析到 root 本身,于是 DELETE ?path=&recursive=1 删掉整个工作区之后还回 {ok:true}。现在创建、重命名、删除都直接拒绝 root;列出 root 仍然没问题。

所有会改动的路由,只要 writeEnabled 没开就一律拒绝;开了之后还必须依次通过:沙箱模式(read-only 拒绝一切)、受保护名称与受保护路径段清单(.env、auth.json、id_rsa、.git/、.ssh/、node_modules/……)、读 root 的包含围栏,以及沙箱策略的可写 root(工作区 root、/tmp、操作系统临时目录)—— 除非模式是 danger-full-access。重命名会同时检查目标和来源。超过 32 MiB 的 Body 在流式传输过程中就被拒绝,不会写入任何东西。

终端协议 —— 裸字符串是输入按键、返回终端输出;JSON 是控制指令:

- 客户端 → 服务端:{type:'create'[,cwd]}、{type:'switch',sessionId}、{type:'close',sessionId}、{type:'resize',cols,rows}
- 服务端 → 客户端:{type:'created',id,pid,shell}、{type:'switched',id}、{type:'exited',id,exitCode}、{type:'error',message}

第一个 shell 的目录挂在握手 URL 上(…/pty?cwd=<dir>);之后新开的标签页在 create 里带上自己的 cwd。无论哪种方式,宿主都会在启动前对目录做围栏检查(见下文),所以不可用的值会回退到工作区 root,而不是被照单全收。

设计说明

写入目标不能是符号链接。 根围栏会对目标做 realpath,这样能抓住指向根外的符号链接——除非目标还不存在,那就没有东西可 realpath,于是改为锚定父目录。而悬空符号链接作为路径最后一段时,父目录在根内,所以能通过检查,随后的一次写入(upload 用的是裸 writeFile)就落到了链接指向的地方——根外,所有可写沙箱根之外。修复前已确认:通过预先埋好的符号链接上传,能把文件写进 $HOME。现在 write、upload 和 mkdir 都会对目标做 lstat,遇到符号链接直接拒绝(403 symlink_target);符号链接出现在最后一段之外的任何位置本来就会被抓住,因为父目录的 realpath 会跟随它。rename 和 delete 仍然作用于链接本身,这正是它们的意义所在。

文本即 UTF-8,而且会做检查。 解码从不抛错——非法字节会变成 U+FFFD——所以 GBK 编码的中文文件本来会被当作乱码送出,看起来却像文件的真实内容。read 路由改为对解码结果做往返校验,不通过就返回 415 not_utf8,预览里会用文字说明原因。

文件名搜索会做 Unicode 归一化。 macOS 存储文件名时用分解形式(NFD),而人们输入的是组合形式,所以 café 会找不到一个字面就叫 café 的文件;现在匹配前两边都归一化为 NFC。

工作区里的字节不能给应用下脚本。 工作区里满是这边没人写过的文件——克隆下来的仓库、某个包的资源、智能体刚生成的东西——而 bytes 会把它们以应用自身的源返回。SVG 是文档,不只是图片:导航到一个含 的 SVG,过去会以应用身份执行它,能伸手进应用的存储,也能反过来打这个 API,包括 PTY 路由(修复前已在浏览器中确认)。该路由现在会发送 Content-Security-Policy: default-src 'none'; …; sandbox 和 X-Content-Type-Options: nosniff,脚本被直接拒绝,文档无论如何都落在不透明源里。应用内预览不受影响——它通过 嵌入这些文件,而 从不执行脚本。HTML 根本不在 MIME 允许列表里,所以工作区里的 .html 会下载而不是渲染。
沙箱 iframe 里的 HTML 预览,默认惰性。 bytes 依然拒绝把 HTML 当文档提供;预览换一种方式渲染它,而 就是整条信任边界。文件文本进入 srcdoc,所以这个 frame 永远是不透明源,而且默认惰性:sandbox="" 什么都不授予,页面里任何脚本都不运行——你看到的是结构和内联样式。按文件开启的 Enable scripts 选项会切到 sandbox="allow-scripts",并且绝不与 allow-same-origin 同时出现——这一对组合恰恰会让 frame 里的页面反过来触达应用,重新打开上面那个洞。即使脚本开着,不透明源也意味着页面碰不到应用的 DOM、cookie 或存储,不能导航顶层 frame,也读不到 workbench API(它没有 CORS);在浏览器里确认过,一个试图设置应用标题并写它 localStorage 的页面两件事都没做成,而它自己 body 里的脚本照常运行。切换这个选项会重新挂载 frame,因为在存活的 srcdoc frame 上改 sandbox 不会重新加载它。View source 退回只读块,Edit 仍然编辑原始标记。

根定在会话的目录。 文件树在你正在查看的会话目录里打开,切换会话时重新定根;终端也在那里启动它的 shell。目录来自客户端自己的会话存储(useSessions——没有新拉取任何东西),并映射到包含它的那个可读根上,所以浏览器向宿主请求的永远只是围栏已经允许的 root+path。客户端向终端请求的 cwd 和任何路径一样不可信:resolveCwdWithinRoots 要求它是绝对路径,并且 realpath 落在允许的根内(做了规范化,所以符号链接的 cwd 会在到达 node-pty 之前被跟随),其他情况一律回退到工作区根。会话目录不在围栏覆盖范围内时,就直接在工作区根打开,而不是失败。

头部启动器通过事件桥驱动界面。 界面和侧边栏启动器都是根作用域,共用一个 workbenchStore;对话头部的启动器是会话作用域,而一个 store 句柄只能挂载在一个作用域上(「一句柄,一作用域」),所以会话座位绑不了根 store。它改为派发一个 window 事件(dsh-workbench:toggle),而始终挂载的 overlay——关闭时渲染 null 但保持挂载——监听它并调用 store action。同一条桥还承载 open-file 请求(一个预览目标),它已经端到端接好并测试过,但目前没有座位派发它:dsh 自己产出的文件信号并非在每个配置档里都加载,而且按设计只跟踪结构化的文件编辑工具(不跟踪 bash 步骤写的文件),所以 artifact-link 界面被砍掉了,而不是半覆盖地发出去。
对话里的文件链接在面板中预览,不在宿主机上打开。 dsh 自带的文件链接——消息里行内的路径提及、每轮下方生成的文件 chip、工具卡片上的路径——全都走宿主机打开器:文件在 dsh 运行的那台机器上打开,从远程浏览器访问时毫无用处,而这是个 Web 应用。没有受支持的办法把它们重新指向别处(提供该功能的服务拒绝第二次注册——会导致插件加载失败,已验证——而且那些按钮不带稳定的 data 属性,只有编译后的哈希类名,本插件不会拿它做键),所以工作台改为拦截点击:一个文档级捕获监听器,作用域限定在对话列和详情列,纯粹按内容识别链接——一个按钮,或者 dsh 留着没激活的行内代码 span,其 title 或整段文本形如文件路径。第二种情况很关键:dsh 只会为它跟踪过的文件生成链接,所以 bash 步骤写出的路径仍是普通 ——恰恰是那种没有别的办法打开的文件。能解析成功的路径会在悬停时加标记,看起来可点击;而只是完成一次文本选择的点击则不予处理。路径会针对读取根目录和会话 cwd 解析并 stat;如果指向真实文件,停靠面板就会打开,只显示预览(没有目录列表),用 Back 恢复浏览。再次点击同一链接会关闭预览,并恢复面板此前显示的内容。解析不到任何东西的路径什么也不做——被替换掉的正是宿主机打开器这一行为,绝不作为回退。按设计就是尽力而为:启发式基于内容,所以未来的 dsh 可能渲染出这里漏掉的链接。

每个文件都能访问,连那些什么都渲染不了的文件也不例外。 PDF 在浏览器自带的查看器里预览(字节路由已经把它们标为 application/pdf;它们跳过了文本读取,而正是那一步以前让它们落得 binary_file 的死胡同),而且每个预览——文本、图片、PDF、二进制拒绝都一样——都带一个 Download,它命中同一条字节路由并加上 download=1,也就是 content-disposition: attachment。附件是更安全的处置方式:附件永远不会被当作文档渲染,所以其他响应头防范的存储型 XSS 问题根本不会出现。严格的 default-src 'none'; … sandbox 策略对 PDF 依然生效——已在 Chrome 中验证其查看器在该策略下渲染正常,所以为了让这套机制工作,没有放宽任何东西。
停靠时不改动布局。 右侧停靠区在框架上占一条,而不是浮在聊天之上。overlay 组件给 AppFrame 打上标记(data-workbench-docked),并把宽度发布为 --wb-dock-width 自定义属性;全局样式表以该属性和布局自身的结构约定为键——对话区是框架的第二个网格子元素,绝不用编译后的 CSS-Module 哈希类名(那些每次上游构建都会变)——把对话列按该宽度推开,并把面板钉在腾出的条带上。侧边栏自身的宽度不动,所以这是真正的分栏,不是覆盖。全屏模式不设置其中任何一项,面板保留 inset: 0,像以前一样盖住框架。锚点相同,纪律也与 mobile-shell 处理其抽屉时一致。

截断的列表仍然显示目录。 上限在排序之后生效,而不是作用于文件系统返回的原始流。若改为截断流,返回的会是文件系统碰巧先给出的那些名字——随机,之后又排了序,所以只是看起来有序——在超过上限的目录里,这可能丢掉所有子目录,而看不见的文件夹就是打不开的文件夹。先收集名字和类型(不逐个发起系统调用),再排序、截断;只对幸存者做 stat,所以无论哪种方式,昂贵的那一遍都受上限约束。两万条目的目录在 50 ms 内列完。

符号链接报告为符号链接,外加它指向什么。 浏览器需要目标的类型,才能知道点击应该跳转还是预览,而读取路由用 stat 而非 lstat,所以指向目录的链接会以目录身份被拒绝——否则检查通过,fs 接缝抛错,用户拿到一个什么也解释不了的 500。指向根之外的链接仍然过不了围栏。

搜索是有界遍历,不是 ripgrep。 harness 把 rg 内置在 tool-fs-search 里,但那是从它自己的树解析的内部依赖,而匹配文件名并不需要它。遍历是广度优先,所以截断的搜索仍会返回用户想要的那些浅层匹配,跳过 .git / node_modules 之类,绝不进入符号链接的目录(循环防护与围栏合一),并在 200 个命中、20 000 个条目或两秒时停止——以先到者为准,到点时会说明。

读取需要自己的围栏。 harness 的 fs-sandbox 只守写入——读取直接透传到本地文件系统。这里的每条路径都解析两次:先做词法遍历拒绝,再做 realpath 包含检查,所以根内的符号链接无法指向根外。
回环检查不等于来源检查,终端就是一个 shell。 WebSocket 握手和跨站 fetch 都不受同源策略拦截,而且都来自用户自己的浏览器——所以 loopbackOnly 看到回环就放行。用户只是访问了一个页面,这个页面就能打开 PTY 并执行命令(ws://127.0.0.1:…/plugins/workbench/pty),或者,在开启写入时,把文件 POST 进工作区。这一点已端到端确认:一个位于不同端口的页面成功到达 created 并执行了 id。现在升级请求和会改状态的 HTTP 动词都会检查 Origin——浏览器总会发送它,同源的会与 Host 匹配,而没有 Origin 的请求不是浏览器,直接放行。跨源握手在 shell 启动之前就被拒绝;跨源写入返回 403 cross_origin。

终端不使用 ctx.terminals。 那个注册表是面向智能体的 PTY 接口:TERM=dumb,输出时剥掉所有 CSI/OSC 序列,重写提示词,每个会话同时只允许一次发送,而且每次调用都必须带上所属的 Agent。这四点对模型来说都对,对 xterm.js 来说都错。这个插件坐落在更下面一层,直接处理原始 PTY 字节。

node-pty 的 spawn-helper 拿回可执行位。 基于 store 的安装(pnpm)可能把 spawn-helper 留成 0644,之后每次 spawn 都失败,只报一句 posix_spawnp failed.,什么也没说明。网关在第一次 spawn 之前会检查并 chmod 它。

node-pty 是借用,不是内置。 dsh 已经安装了它;网关先从这个模块解析,再从 ctx.baseUrl(配置档目录)解析,因为用 link: 安装的插件从自己的 node_modules 里看不到配置档提升后的依赖树。

窗格的规则住在窗格之外。 帧分类、退避、标签页与活动会话的切换,都是 src/client/terminal-model.ts 里的纯函数;组件只保留 socket、xterm 实例和定时器。正因如此,重连行为才能在不把 xterm 塞进 jsdom 的情况下测试。

销毁时挂断。 WebSocketServer.close() 会让已建立的 socket 保持打开,所以插件卸载会杀掉 shell,而浏览器还握着一个指向已不存在网关的 socket——没有 close 事件,因此不会重连,终端就此永久冻结。销毁流程会先用 1001 关闭每个 socket,再关停服务器。

终端会重连,而且会说出来。 socket 断开意味着服务端的 PTY 也随之死亡,所以窗格会清空标签页,而不是把死标签留在屏幕上,显示一行带手动重试的状态,并按 1/2/4/8/15s 退避重连——dsh 重启或笔记本休眠之后,shell 会自己回来。

终端输出里的 URL 可点击,靠的是 xterm 的 web-links 插件——工具会不停打印它们,手动选中一个实在麻烦。
预览复用 harness 原语 —— ReadBlock(行号 + shiki)、MarkdownText(GFM + KaTeX),以及主题的 --dsw-alias- token —— 而不是再引入一套 markdown 或高亮栈。

编辑

打开 writeEnabled 后,浏览器端会多出一条操作栏(新建文件、新建文件夹、上传),可以接受拖到列表上的文件(放进当前打开的目录,而不是根目录),并且,点击某个条目的图标将其标记后,可以重命名和删除。删除是在同一个按钮上两步操作:先预备、再确认,而不是弹对话框。文本预览会多出 Edit 按钮、一个纯 textarea 和 Save(Cmd/Ctrl+S),保存后列表刷新。关闭写入时,这些全都不渲染 —— 浏览器向 /health 询问这个标志,而宿主无论如何都会强制执行。

测试

pnpm test 让宿主端跑在真实 socket 和真实临时目录上(路由、两道围栏、每一个状态码),让终端网关跑在真实 WebSocket 和真实 shell 上(控制协议、resize 传到 pty、shell 随其 socket 一起退出),让浏览器端跑在 jsdom 里(操作栏的行内命名与先预备再确认的删除、列表与导航、能力门控)。不需要 API key,也不需要运行 dsh。

配置错误在加载时就失败,而不是等到点击时。 readRoots 在插件挂载时校验 —— 每个坏条目一次性全部点名 —— 因为另一种做法是:某个根目录留在选择器里,对每个请求都回一个莫名其妙的 404;也因为 harness 自己的规则是,缺失的指称对象绝不静默跳过。

预览只铺开一个窗口,而不是整个文件。 原语自己的默认值是 16 行 —— 对工具结果卡片合适,用来阅读则没用 —— 而另一个极端更糟:长文件的全部 30 000 行是 90 000 个 DOM 节点,之后每次重新渲染都要 46–154 ms 重排,而 2 000 行的窗口是 6 000 个节点、6–11 ms。头部会说明你正在看文件的多少,剩下的内容切一个标签页就是终端。

打开的预览也能跟上,而且不会吃掉你的草稿。 面板轮询开销很小的 stat 路由,只有版本变了才重新读取。如果你正在编辑,它绝不碰 textarea —— 它会说文件在磁盘上变了,让条件保存去当那个拒绝覆盖的角色。

迟到的轮询无法撤销你刚做的事。 每次变更都会递增一个 generation 计数器,请求发出早于该变更的轮询会丢弃自己的结果 —— 否则,在删除前一刻取回的列表会在删除之后落地,把已删除的文件重新放回屏幕上几秒钟。

列表跟得上智能体。 文件会出现在你正看着的目录里,所以面板按间隔并在获得焦点时重新检查,而不是让你切走再切回来。标签页隐藏时什么都不做 —— 浏览器本来就会节流后台定时器,而焦点处理器会在你回来的那一刻补上。
关掉的功能会自己说明。 health 同时上报 writeEnabled 和 ptyEnabled;终端关闭时,面板会说明部署已禁用终端,并且根本不打开 socket,而不是反复重试一条并不存在的升级路径,还把它说成连接断开。

错误信息会按查看者的身份改写。 API 的消息是写给请求日志看的——英文、点名查询参数、描述规则而不是出路——所以浏览器把稳定的错误码*翻译成自己的本地化句子,只对还没学会的码保留宿主的原文。不该让文件浏览器告诉用户「pass overwrite=1」。

测试锁定了哪些日后容易改坏的行为

- 递归删除会解除链接符号链接,而不是深入其中,所以删掉一个包含快捷方式的文件夹,不会把快捷方式指向的目标一起带走。
- 上传中断时,已有文件保持原样——写入任何内容之前会先缓冲请求体,而直接流式写盘会悄悄破坏这一点。
- 文件名里含 CRLF 也无法注入响应头。

已知限制

- 长文件只显示窗口。 预览只排版前 2 000 行,并会说明这一点;超过这个长度的 markdown 回退到源码视图,因为 markdown 渲染器自身没有窗口机制。
- 编辑器就是 textarea。 编辑时没有语法高亮,没有自动保存,没有冲突检测:一次保存会覆盖磁盘上的任何内容。
- mkdir/rename/delete 绕过了 fs 接缝。 ctx.fs 没有对应能力(它只暴露 read、write、edit),所以这三个操作走 node:fs,唯一的关卡是本插件自己的防护——不像 write,它还会额外经过 harness 的每次调用写入策略。
- 会话会保留它启动时的沙箱模式。 之后再降低模式,不会追溯限制一个正在运行的 shell——关掉它,再开一个新的。
- 没有远程运行时。 启动直接走 node-pty,而不是 ctx.subprocess.spawnTerminal,因为那个接缝的句柄没有暴露 resize()(rows/cols 在启动时就固定了)。因此终端只在宿主进程运行的地方可用。
- 重连不会恢复原来的 shell。 它们随 socket 一起消失了;你会拿到一个全新的,断开之前的回滚缓冲也丢了。
- 刷新是轮询,不是监听。 列表和任何打开的预览在可见时每五秒重新检查一次,标签页重新获得焦点时立即检查;没有文件系统监听,所以变更最多要五秒才显示。搜索会暂停列表轮询。
- HTML 预览是沙箱化的,默认还是惰性的。 除非你按文件选择启用,否则脚本被阻止,即便启用也在不透明源里隔离运行。因为 frame 没有 base URL,也没有同源访问权限,页面自己的相对或外部资源——它链接的 CSS/JS/图片——可能加载不了:这是结构和内联样式的预览,不是活的站点。要看标记请用 View source。
- 链接拦截靠的是内容启发式判断。 文件链接按钮是根据标题/文本是否形如路径来识别的(它们身上没有稳定的钩子),所以 dsh 一旦改动链接的渲染方式,部分链接就可能退回由宿主打开器处理,直到启发式规则更新为止。
- 不支持 Range 请求。 bytes 会发送整个文件,因此大体积媒体无法拖动进度条;它面向的是图片和小文件下载。
- 只有 UTF-8 文本能显示。 其他编码会被检测出来并拒绝,而不是转码;没有编码选择器。
预览分享卡片长什么样 ↓ghbhiee/dsh-plugin-workbench
分享 · DeepSeek Harness Hub
ghbhiee/dsh-plugin-workbench

在会话侧栏浏览文件、预览内容并打开终端

回到顶部