在侧栏直接编辑、保存和删除工作区文件
29.7/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/22 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add dongasai/dsh_editor该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:仅索引
图例:✓ 通过! 需注意— 未判定 / 无数据· 「未判定」= 本站尚无数据,不代表插件有问题
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
README
由 DeepSeek 最新模型翻译生成@dongasai/dsh-editor
在 DSH Web GUI 的右侧栏里直接把工作区文件改掉:官方文档预览注册的 Markdown / 代码 / 图片 / PDF / Office 渲染器全部只读,读文件的 Remote 命名空间 @deepseek-ai/dsh-api-workspace-files 也明确写着 "The service exposes no mutation operation.",这个插件补上写与删的那一半 —— 可编辑的正文、保存按钮、未保存状态与冲突处理,预览头部的「🗑 删除」按钮(两次点击确认),外加一个自带文件树的右侧栏工具 「文件编辑」:新建、改名 / 移动、复制 / 剪切 / 粘贴、删除、拖拽整理,都在 GUI 里做完 —— 包括把系统剪贴板里复制好的文件直接粘进工作区。它们全部打到本插件自己的 Host 路由。
与「用终端 / 让 Agent 改」的差别不是快慢,而是谁在改:人在 GUI 里改完点保存,版本闸记得住「你读到的是哪一版」,磁盘中途被模型改过就停下来问,不静默覆盖;删除同样带版本闸 —— 你确认期间文件被改过就停下来,要删得再点一次「仍然删除」。
- 怎么用插件 → 本文件(安装、怎么进编辑器、线上协议、排错、已知边界)。
展开完整文档(README 剩余 1 节)
@dongasai/dsh-editor
在 DSH Web GUI 的右侧栏里直接把工作区文件改掉:官方文档预览注册的 Markdown / 代码 / 图片 / PDF / Office 渲染器全部只读,读文件的 Remote 命名空间 @deepseek-ai/dsh-api-workspace-files 也明确写着 "The service exposes no mutation operation.",这个插件补上写与删的那一半 —— 可编辑的正文、保存按钮、未保存状态与冲突处理,预览头部的「🗑 删除」按钮(两次点击确认),外加一个自带文件树的右侧栏工具 「文件编辑」:新建、改名 / 移动、复制 / 剪切 / 粘贴、删除、拖拽整理,都在 GUI 里做完 —— 包括把系统剪贴板里复制好的文件直接粘进工作区。它们全部打到本插件自己的 Host 路由。
与「用终端 / 让 Agent 改」的差别不是快慢,而是谁在改:人在 GUI 里改完点保存,版本闸记得住「你读到的是哪一版」,磁盘中途被模型改过就停下来问,不静默覆盖;删除同样带版本闸 —— 你确认期间文件被改过就停下来,要删得再点一次「仍然删除」。
- 怎么用插件 → 本文件(安装、怎么进编辑器、线上协议、排错、已知边界)。
- 为什么这么实现 → IMPLEMENTATION.md(设计决策、两半怎么拼起来、四条实现约束的来龙去脉)。
快速开始
两步:装插件 → 在右侧栏打开一个文件。
1. 装插件
三种装法,按场景选。都走同一条 pnpm 路径(@deepseek-ai/dsh-plugin-manager/operations,共用一把 profile 写锁),装完都会出现在侧栏「插件」页的已安装列表里。
都不需要手写注册行:本包在 package.json 里声明了 dsh.bundle.patch,只要这个 bundle 被选中,生效的那一行就来自本包自带的 cordis.patch.yml。命令行(方式二、三)安装会顺带把它选进 dsh.profile.bundles;Web 上(方式一)是装完点 立即启用才选中 ——不点就只是「装着但没启用」。
本包 private: true、未发布到 npm,所以没有「按包名从 registry 装」这条路:spec 要么是本地路径,要么是 git 地址。
方式一:Web 界面(推荐)
DSH Web → 左侧栏插件 → 添加插件 → 填入本仓库绝对路径 /data/ai/dsh/dsh_editor (或下面的 git 地址)→ 安装 → 装完点立即启用。
- 「添加插件」接受:包名(可带版本)、Git 地址、压缩包、本地绝对路径。
- 装完不点立即启用,就只是「装着但没启用」。
- 侧栏这个「插件」页才能装和开关;设置 → 内置插件里的清单是只读的。
- 填 git 地址时若 GitHub 连不上,界面会提示无法访问 GitHub 并给出改用国内镜像的选项(镜像只提供注册表里的包与依赖,不代替 GitHub 仓库下载)。
方式二:命令行 · git 源(分发)
dsh plugin --profile web add github:dongasai/dsh_editor
或
dsh plugin --profile web add git+https://github.com/dongasai/dsh_editor.git
dsh plugin --profile <name> <pnpm args> 把参数转发给该 profile 目录里的 pnpm。装进来的是 副本:之后改本仓库代码不影响已装的副本(想边改边用就走方式三)。
- --profile 必填(不写直接报 required option '--profile <name> not specified'); web 换成你要装进的 profile。
- 钉版本:URL 后加 #,例如 git+https://github.com/dongasai/dsh_editor.git#v1.1.0(tag / 分支 / commit 都行)。
本仓库当前 tag 是 v1.1.0,对应 package.json 的 1.1.0 —— 不带 # 就跟 main 走,每次装到的都可能是新代码(本机开发用方式三的 link:,不受影响)。
- 私有仓库:git+ssh://git@github.com/dongasai/dsh_editor.git(需已配好 SSH key),或先把 HTTPS 凭据配好。
- 必须带 git+ 或 github: 前缀。git@github.com:owner/repo.git 这种 scp 写法不会被认成
git spec,pnpm 会把它当路径,装出一条指向不存在目录的 link: 死链(本机 profile 里就留着一条
这样的残留:依赖名 git → link:github.com:yangshen830-eng/dsh-editor.git,node_modules/git 是断链)。CLI 识别 git spec 的判定也是这个:/^git\+|^github:|\.git(?:#|$)/。
- pnpm 默认会拦 prepare 等依赖构建脚本;本包没有任何构建步骤(client.js 是手写的浏览器 bundle,由 Host 直接读盘发出),不会遇到。
- git / tarball 这类 spec 要先 fetch 才知道 peer 范围,所以 DSH 版本兼容判定发生在安装之后;判定不兼容会回滚 package.json 与 pnpm-lock.yaml 并打印 allow-version 命令。本包没有声明任何 DSH peer 依赖,不会触发。
方式三:命令行 · 本地目录(link,开发)
dsh plugin --profile web add link:/data/ai/dsh/dsh_editor
装成 link: 依赖,代码改动立刻是「已装的那份」—— 适合边改插件边用。
- 改 Host 半边(index.js):要重启 Harness 才生效(见「已知边界」)。
- 改 浏览器半边(client.js):刷新页面即可(bundle 每次请求都从磁盘现读,按内容 revision 换 URL),不需要重启。
装完确认
Config.listConfigs(name: "@dongasai/dsh-editor")里能看到行、且 enabled 为真,即已生效。
再新开一个会话(或重启 Harness)让插件加载,然后按第 2 步在右侧栏验证。
2. 在右侧栏用起来
1. 打开右侧栏的「文件」页,点开任意一个文本文件。
2. 默认就以编辑器打开的文件:.txt .text .log .json .jsonc .json5 .jsonl .ndjson .yaml .yml .toml .ini .cfg .conf .config .env .properties .lock —— 官方对这些文件本来也只有只读纯文本 / 代码预览,没有不可替代的能力,所以直接给编辑器当默认视图,点开就能改。
3. 其它文本 / 代码文件(.md .ts .py .sh .html .css .sql .csv …):默认仍是官方的排版 / 高亮预览。要编辑,点预览工具栏上的「✎ 编辑」按钮 —— 它用编辑器页签顶掉这张预览页签(同一个面板、同一个位置);该文件的编辑器页签已经开着时只聚焦它。也可以走 「打开方式」下拉选「编辑器」,那是就地换渲染器,选择按标签页记住。
4. 改完点「保存」,或按 Ctrl/Cmd+S。
5. 要删掉这个文件:点预览工具栏上的「🗑 删除」(在「✎ 编辑」右边)。第一下只是把它变成「确认删除」,第二下才真的删 —— 中间那几秒里如果你不点,它会自己变回「删除」。
删掉的文件不进回收站,页签会随之关掉,文件树里那一行也会消失(树自己盯着变更)。
6. 把文件与行号发到对话输入框:在编辑器正文里右键,选「发送到 DSH 输入框」 ——它把「当前文件 + 选区所在行」拼成 @路径#L行号(选中多行是 @路径#L起-止,没有选区就是光标所在行)插进当前会话的对话输入框,插在输入框光标上次所在的位置。工作区内的文件按相对路径写,工作区外用绝对路径;路径带空格时按官方 grammar 加引号。插进去的是普通文本(不是文件芯片),模型侧按 @…#L 引用语法解析。
3. 用「文件编辑」面板
右侧栏的加号 / 引导页里有一张「文件编辑」卡片(快捷键 Ctrl/Cmd+Alt+E,可在快捷键设置里改)。
它开一个自带文件树的页签:
| 想做什么 | 怎么做 |
| 看目录 | 点目录行展开 / 收起;条目默认「目录在前、再按名字」(工具条上的排序按钮可切成「按类型」,或关掉「目录优先」);一路只有一个子目录的链会折成一行(a/b/c,点它展开链尾);名字以点开头的条目淡显;目录变了会自己刷新(订阅了官方变更流) |
| 找操作 | 每行行尾有常显的「⋯」(不用悬停),行上也有悬停出现的图标;右键任意一行是一份完整菜单 |
| 打开文件 | 单击文件行 → 在同一栏里开本插件的页签(面板自己不消失):文本进编辑器页签,图片进图片预览页签(见下) |
| 新建 | 工具条上的「新建文件 / 新建文件夹」落在当前选中的目录(选中文件时是它所在的目录);也可以点某个目录行上的 + |
| 改名 / 移动 | 四个入口:行上铅笔(悬停出现)、行尾常显的「⋯」菜单、右键菜单、工具条上的铅笔(作用于选中行)。输入框里回车提交,名字里带 / 就是搬到子目录(a/b.md);键盘党按 F2 |
| 创建副本 | 文件行上的复制按钮:同目录生成「xxx 副本」,重名自动往后编号;目录不支持复制 |
| 复制 / 剪切到剪贴板 | 「⋯」或右键菜单里的「复制到剪贴板」(只收文件)与「剪切」(文件与目录都行);键盘党在树上按 Ctrl/Cmd+C / Ctrl/Cmd+X。剪贴板贴着面板底部显示,带一个「清空」 |
| 粘贴 | Ctrl/Cmd+V 贴系统剪贴板里的文件(在系统文件管理器里复制一个文件,回面板按一下就行);菜单 / 工具条上的「粘贴」也贴同一份。有内部剪贴板时优先贴内部那一份:复制过来的是「再复制一份」(重名自动编号、剪贴板留着可以连贴),剪切过来的是移动(贴完剪贴板清空) |
| 复制路径 | 「⋯」或右键菜单里的「复制路径」(这个条目的绝对路径)与「复制相对路径」(相对工作区根,如 sub/keep.txt)。成功时面板上方亮一条一次性提示(几秒后自己消失);写剪贴板失败时亮同一行提示但不会自动消失(带「取消」点掉) |
| 在终端打开 | 「⋯」或右键菜单里的「在终端打开」:在右侧栏开一个终端页签,等它的 shell 一变得可写就替你敲一条 cd '<该目录>'(目录行 = 它自己,文件行 = 它所在的目录),终端里能看到这一行。见下面「为什么是敲一条 cd」 |
| 删除 | 行上的垃圾桶按钮 → 确认条再点一次。空目录一次确认;非空目录确认条会报条目数,按钮是「递归删除」 |
| 拖拽 | 把行拖到目录行上松手 = 搬进它;拖到文件行上 = 搬到它所在的目录(所以拖到根下任意一个文件行就是搬到根);一开始拖,工具条下面会亮出「工作区根目录」落点条,丢到它上面(或树正文的空白处)就是搬到根目录。拖到自己身上、拖回原地、把目录拖进自己的子树都不接受 |
| 多选 | 每行左侧有一颗常显的勾选框 —— 点它就是多选的主入口(不用知道快捷键)。也可以 Ctrl/Cmd+点切换、Shift+点按可见行顺序取一段(这两种都不打开文件)、Ctrl/Cmd+A 全选可见行、Esc 或点空白处清空。普通单击只移动光标、不产生勾选。勾了至少一项就在工具条下方出现批量条:最左是全选框(全选 ✓ / 半选 −),后面是已选 N 项 / 复制路径 / 删除 / 清空 |
| 批量删除 | 批量条上的「删除」→ 一次确认条(里面有目录时会写明「会连内容一起删」)→ 逐条串行删除,每条都带自己的版本闸。部分失败时失败项留在选择里,并亮一条带「重试删除」的横幅(重试会重新取一遍基线,所以磁盘变过的那几条也能接着处理) |
为什么是「敲一条 cd」而不是「让终端在该目录启动」:官方终端接口(remote.terminal.create)只有 shellPath,没有 cwd —— 侧边栏终端一律从会话工作区根 起步,页签参数也只有 terminalId / shellPath。所以这个动作是「开页签 → 等它可写 → 替你敲一条 cd '<绝对路径>'」:你能在终端里看到这行命令,之后就在那个目录里了。如果这个部署里没有侧边栏终端(终端页签类型没注册),菜单项会直接说明,不会装作开了个终端。
删除同样带版本闸:你点「删除」的瞬间它会记下这个条目的版本,等你点确认时如果磁盘上已经变过(模型或别的工具动过),Host 会拒绝,确认条上改说「这个条目在确认期间变过,没有删除」并把按钮换成「仍然删除」——要删得再点一次。
批量删除刻意没有「整批仍然删除」这种口子:一次点下去就把 N 个条目全部无条件推平,比逐条重试危险得多。所以部分失败时它只做两件事 —— 把失败项留在选择里、给一条带「重试删除」的横幅;重试会 重新取每一条的基线,于是「确认期间被改过」的那几条也能在你看过之后删掉。
排序与折叠都不改数据:排序只改渲染顺序(换一次排序键不会重新读盘);折叠只用已经读过的层级 —— 为了知道「这一层是不是只有一个子目录」,它会对可见层级里那些还没读过的目录各探一次(每次挂载最多 64 个,链一旦不是「唯一子目录」就停),但不会给预读到的目录订阅变更:折叠标签只是显示,真要内容还是得展开(展开时才订阅)。
切走再回来不丢版面:这个面板注册成 keepMounted,点开一个文件(切到编辑器页签)再点回来, 展开的目录、滚动位置、光标都还在;以前每次回来都会重挂、缩回根一层。
面板最底部固定一行插件信息(工作区文件编辑器 · v1.1.0 这样),鼠标放上去显示插件的介绍;标题与版本号是从 Host 的插件清单里现读的(按包名认领自己那一行),所以升级插件后这里自己就变,不需要谁去改客户端;部署里没有插件管理器(读不到清单)时只显示包名 @dongasai/dsh-editor,关掉面板重开一次会再试一遍。
「没有工作区根」那种情况下页脚同样在底部。
4. 确认它真的在工作
改一个字,底部状态条应当从「已与磁盘同步」变成「未保存的修改」并且「保存」按钮变成品牌色;点保存后变「已保存」,用 cat / git diff 看磁盘,内容确实变了。看不到编辑器时按「排错」表逐条对。
在「文件编辑」面板里新建一个文件,ls 应当立刻看得到它 —— 面板自己的列表也会跟着刷出来。
图片预览
在「文件编辑」面板里单击图片,开的是本插件的图片预览页签(不是文本编辑器 —— 那会得到「这个文件不是 UTF-8 文本」):
- 图片适应窗口显示,工具条上一个「原始大小」开关可以在适配窗口 / 原始大小之间切(原始大小时超出部分靠滚动看)。
- 工具条左侧是文件信息:字节数,解码出来之后还有 宽×高 与 MIME 类型。
- 右键有「复制路径 / 复制相对路径 / 在终端打开」—— 与文件树、编辑器菜单调的是同一份实现。
- 后缀认得出、但浏览器解不开的格式(tiff / heic 这类,本插件不内置解码器):明确说「浏览器解不开这个格式」,并给出「重试」与「用编辑器打开」两个出路(后者对 svg 看源码很顺手)。
- 读盘失败(文件被删 / 移走)时同理:显示 Host 的失败码与说明 + 「重试」。
- 预览工具栏上的「✎ 编辑」对图片不显示 —— 编辑器对图片给不了任何东西。
官方那个图片渲染器完全不动:本插件只补自己的入口(「文件编辑」面板的单击)。官方文件树、官方的自动打开、官方右上角的「打开方式」下拉都还走官方图片预览;图片也不进「哪些文件能进编辑器」那两张表。
编辑器长什么样
状态条会显示「已与磁盘同步 / 未保存的修改 / 保存中… / 已保存」,紧跟着这个文件的相对路径 (与右键「复制相对路径」同一份口径:工作区内显示成 docs/README.md,工作区外的文件自动退回绝对路径);太长时从头部截断以保住文件名(…/that/going/README.md),完整绝对路径在鼠标悬停的 title 与 [data-workspace-editor-status-path] 上。标题栏右侧是「重新加载」(丢弃本地修改、重读磁盘)与「保存」。编辑器页签里多一个「换行:开 / 关」(页签窄,默认开)。
图片预览那一面也显示同样的路径(同一份实现、同一个锚点)。
正文里右键有一份菜单,三组:剪切 / 复制 / 粘贴 / 全选,分隔线后的 「发送到 DSH 输入框」(禁用时把鼠标放上去会说明原因),再一条分隔线后的 「复制路径」「复制相对路径」「在终端打开」 —— 作用对象是正在编辑的这个文件:路径优先用读盘拿到的绝对路径,「相对」是相对会话工作区根;「在终端打开」打开的是它所在的目录(文件在根目录下就是工作区根)。这三项与「文件编辑」面板里的同名菜单项共用同一份实现 (含失败话术),只是横幅渲染在编辑器自己的位置。
正文带语法高亮:输入框的字是透明的(只留光标与选区),看得见的字形来自它底下那层等宽文本,颜色用主题的 --shiki- 令牌(与官方代码预览同一套配色,亮 / 暗主题都跟着走)。语言按后缀 判:JS / TS / JSON / HTML 系(含 Vue、Svelte、XML、SVG)/ CSS 系(含 SCSS、LESS)/ Markdown / YAML / INI 系(含 TOML、.env、properties)/ Python / Shell 系(含 PowerShell、批处理)/ SQL / diff / patch,以及 C 家族(C/C++/Java/C#/Go/Rust/Kotlin/Swift/PHP 等,按近似的关键字表上色);认不出的后缀(.txt、Dockerfile、没有后缀的文件)按纯文本显示。
两个上限与一个前提:超过 256 KiB 字符或 2 万 token就不上色(正文照旧能编辑、能存), 上色用 Custom Highlight API(Chrome 105+ / Safari 17.2+ / Firefox 140+),不支持它的浏览器按
纯文本显示。当前状态可以直接在元素上看:[data-workspace-editor-highlight] 是
on / plain / over-limit / unsupported。取舍与实测代价见 IMPLEMENTATION.md。
「✎ 编辑」按钮换的是页签而不是就地切换渲染器:预览用哪个渲染器由文档预览自己的 store 决定(state.rendererId),外部插件没有公开的写入入口;右侧栏的页签类型是公开的,所以按钮走 ctx.sidebarRight.openResource(address, { kind: 'workspace-editor', replaceTab })。
想回预览:从文件树再点一次该文件(会开新的预览页签)。
编辑器页签类型刻意声明了一个命中不了任何真实地址的 pattern,因此它永远不会参与自动排名 —— 普通打开文件仍旧是官方预览,编辑器只在按钮 / 显式打开时出现。
数据不会悄悄丢
- 磁盘变了(模型或别的工具改了同一个文件):不会覆盖你正在编辑的内容,改为顶部横幅提示,让你选「加载磁盘版本」还是「继续编辑」。
- 保存时磁盘变过:Host 的版本闸会拒绝,横幅里让你选「覆盖保存」或「加载磁盘版本」。
- 删除时磁盘变过:从你点「删除」到点「确认删除」之间文件被改过,Host 一样会拒绝 ——按钮变成「仍然删除」,要删得再点一下,不会稀里糊涂删掉别人刚写的东西。
- 切到别的标签页、或关掉编辑器页签再打开:未保存的草稿还在(草稿按 会话 + 路径 缓存在浏览器内存里)。
- 读盘临时失败:已经显示着正文时只挂一条提示,不把正文擦掉(磁盘抽风不该毁掉草稿)。
结构
| 文件 | 作用 |
| index.js | Host 半边:注册六条变更路由(write / delete / mkdir / rename / copy / upload),做信任围栏、路径围栏、沙箱策略与版本闸;写走 ctx.fs 的原子写,其余五条走 node:fs。只 import node: 内置模块。 |
| client.js | 浏览器半边:两档文档渲染器 + 编辑器页签类型 + 预览头部的「编辑」「删除」按钮 + 「文件编辑」工具面板(自带文件树)。 |
| cordis.patch.yml | 只插入一行插件;浏览器半边由 package.json 的 dsh.client 声明,由 dsh-client-modules 扫出来。 |
| package.json | bundle 清单:dsh.bundle.patch + dsh.client。 |
| locale/{zh,en}.json | 插件卡片上的显示名与说明。 |
| icon.svg | 插件卡片图标。 |
Host 半边 inject:webServer / connection / fs / sandboxPolicy / sessions (sessionPersistence 用 ctx.get() 可选取,冷会话才需要)。
浏览器半边占用的扩展点:
| 扩展点 | 注册物 |
| ctx.documentPreviews.register | @dongasai/dsh-editor/text(extension 档,默认视图)、@dongasai/dsh-editor/text-option(builtin 档,下拉备选) |
| sidebar.right.tab.document | 上面两个 id 的正文 |
| ctx.sidebarRightTabs.register | kind workspace-editor,id @dongasai/dsh-editor/tab,pattern 命中不了真实地址 |
| sidebar.right.pane.tab | 该 id 的页签正文(编辑器) |
| ctx.sidebarRightTabs.register | kind workspace-editor-image,id @dongasai/dsh-editor/image,pattern 同样命中不了真实地址(图片的自动打开仍归官方预览) |
| sidebar.right.pane.tab | 该 id 的页签正文(图片预览) |
| sidebar.right.tab.document.actions | id workspace-editor、order: 5 的「✎ 编辑」按钮;id workspace-editor-delete、order: 6 的「🗑 删除」按钮 |
| ctx.sidebarRightTabs.register | kind workspace-editor-files,id @dongasai/dsh-editor/files,guide 卡片「文件编辑」(没有 patterns,只能按 kind 打开) |
| sidebar.right.pane.tab | 该 id 的文件树面板正文(自带新建 / 改名 / 复制 / 剪切 / 粘贴 / 删除 / 拖拽) |
| ctx.shortcuts.register | 命令 workspace.editor.files(默认 Ctrl/Cmd+Alt+E),打开「文件编辑」面板 |
哪些文件能进编辑器
| 档位 | priority | 扩展名 | 打开方式 |
| 默认 | extension | .txt .text .log .json .jsonc .json5 .jsonl .ndjson .yaml .yml .toml .ini .cfg .conf .config .env .properties .lock | 从文件树点开就是编辑器 |
| 可选 | builtin | 代码 / 文档 / 标记 / 表格类(.md .ts .py .sh .html .css .sql .csv 等,见 client.js 的 OPTIONAL_EXTENSIONS) | 只在「打开方式」下拉里出现,不抢默认 |
| 兜底 | — | 能在预览里显示出正文与 Host 路径的文本文件(预览的「不支持此文件」分支里没有这个按钮) | 点工具栏上的「✎ 编辑」按钮 |
priority 是档位而不是排序分值:extension 档按扩展名声明就能当默认,builtin 档只作为显式备选。
没有扩展名的文件(Makefile、LICENSE)进不了编辑器:官方预览匹配不到渲染器时会走它的「不支持此文件」分支,那个分支不渲染 sidebar.right.tab.document.actions 槽位,所以「✎ 编辑」按钮根本不出现。想编辑就给文件名加个扩展名,或从「打开方式」下拉里选(前提是它至少能匹配到可选档)。
线上协议
写请求:
POST /workspace-editor/write
content-type: application/json
{
"sessionId": "session-3", // 用来定位会话工作区根与沙箱策略 "path": "src/main.ts", // 相对工作区根,或绝对路径 "content": "…完整文件内容…", "expectedVersion": "…", // 读到的版本;省略 = 无条件覆盖(用户点过「覆盖保存」) "mode": "replace" // 或 "create"(要求文件不存在) }
成功(200):{ "ok": true, "path": "/abs/path", "operation": "create" | "update", "version": "…", "bytes": 123 }
删请求:
POST /workspace-editor/delete
content-type: application/json
{
"sessionId": "session-3",
"path": "src", // 文件或目录 "expectedVersion": "…", // 点「删除」时看到的版本;省略 = 无条件删除(用户点过「仍然删除」) "recursive": false // 目录:false/省略只删空的(rmdir),true 才连内容一起删 }
成功(200):{ "ok": true, "path": "/abs/path", "operation": "delete" }
建目录 / 改名 / 复制 / 粘贴上传:
POST /workspace-editor/mkdir { "sessionId": "…", "path": "docs" } // 不递归建父级 POST /workspace-editor/rename { "sessionId": "…", "from": "a.md", "to": "b/c.md" } // 改名或移动 POST /workspace-editor/copy { "sessionId": "…", "from": "a.md", "to": "a 副本.md" } // 只复制文件 POST /workspace-editor/upload { "sessionId": "…", "path": "logo.png", "data": "iVBORw0…" } // base64 → 新文件
成功(200):mkdir → { "ok": true, "path": "…", "operation": "mkdir" }; rename / copy → { "ok": true, "from": "…", "to": "…", "operation": "rename" | "copy" }; upload → { "ok": true, "path": "…", "operation": "upload", "bytes": 1234 }。
upload 是「粘贴系统剪贴板里的文件」的落盘口:正文是 base64(可能是图片、压缩包这类二进制),目标一律必须不存在 —— 它相当于复制(COPYFILE_EXCL),不是保存,没有版本闸、也不接受 expectedVersion。重名由客户端先编号,真撞车由落盘那一步的 wx 拒绝。
失败:{ "code": "…", "message": "…", "fsCode": "…" },HTTP 状态按码给出。六条路由共用下表:
| code | HTTP | 含义 |
| bad-request | 400 | 请求体不是 JSON 对象、字段类型不对(sessionId / path / from / to 必须是非空字符串,content / data 必须是字符串,recursive 必须是布尔),或 upload 的 data 不是规范 base64 |
| outside-workspace | 403 | 目标不在该会话的工作区内(rename / copy 的两端都要在里面) |
| denied | 403 | 沙箱策略拒绝(read-only 会话;写由 ctx.fs 判,其余五条由插件按 ctx.sandboxPolicy.resolve 自己判) |
| unknown-session | 404 | 这个 Host 既不认识活会话,也没有它的存档头 |
| not-found | 404 | 目标不存在,或父目录不存在(写:create 之外的模式;删 / 改名 / 复制:条目已经不在;粘贴上传:父目录不存在) |
| stale-version | 409 | 磁盘上的条目变过(写:编辑期间;删:确认期间)或已被删除 —— UI 写提示「覆盖保存」、删提示「仍然删除」 |
| not-observed | 409 | 写:create 模式下文件已存在 |
| target-exists | 409 | 改名 / 复制 / 粘贴上传 / 新建目录:目标已经存在(不覆盖、不改名;复制是 COPYFILE_EXCL、粘贴上传是 writeFile 的 wx,都是内核级拒绝) |
| not-empty | 409 | 删除:目录不是空的,而这次没有带 recursive: true |
| invalid-target | 409 | 改名 / 复制:源与目标是同一个路径,或把目录搬进它自己的子树 |
| not-regular-file | 409 | 目标是符号链接(一律拒绝)或类型不对(复制与粘贴上传只收普通文件,改名收文件与目录) |
| not-directory | 409 | 路径中间的一段不是目录(父级不存在是 404 not-found) |
| too-large | 413 | 请求体(8 MiB)、内容(4 MiB 字符)或粘贴上传的字节(解码后 4 MiB)超限 |
| unsupported-media-type | 415 | 不是 application/json |
| not-text | 415 | 底层文件系统策略判定不是文本(本插件读盘走 readBytes,不做文本判定,所以这一条只在底层拒绝时出现;非 UTF-8 / 含 NUL 的文件通常由浏览器侧的 TextDecoder 先判掉,见「已知边界」) |
| aborted | 408 | 客户端断开或超过 30s 墙钟上限 |
| write-failed / delete-failed / mkdir-failed / rename-failed / copy-failed / upload-failed / io-error | 500 | 其它文件系统故障(原始信息在 message;后五个是各自路由上未映射的 node:fs errno) |
其它方法一律 405 并带 allow: POST;所有应答都带 cache-control: no-store。
fsCode 只在失败源自底层文件系统时出现(FS_STALE_VERSION 这类原始码),排查时看它比看 message 稳。
安全模型(三道闸 + 一道版本闸)
1. 信任围栏:每个请求先过 ctx.connection.requestRejection(req) —— 不可信 Host/Origin 直接 403,浏览器没认证就是 401。非本机认证页面发不进来。
2. 路径围栏:目标解析后必须落在该会话的工作区根之内
(ctx.fs.contains(会话根, 目标)),并且最终节点不能是符号链接(lstat 检查),
想借软链写穿出去会被 403 挡住。
3. 沙箱策略围栏:写入显式带上 ctx.sandboxPolicy.resolve({ session }),由组合文件系统 ctx.fs 再判一次(read-only 会话一律拒绝,workspace-write 只放行工作区内与平台临时区)。
另外五条没有这层自动保护 —— ctx.fs 没有删除 / 改名 / 建目录 / 复制 / 写二进制的接口,它们走 node:fs,所以插件按同一份 ctx.sandboxPolicy.resolve({ session }) 自己判 read-only
(guardMutation() 一处收口)。
4. 破坏性变更有版本闸:expectedVersion 写时走 FsWriteIntent.replaceIfVersion (或 createIfAbsent),写入是原子的;删时在落盘之前逐字比对同一个版本令牌。版本不符就 FS_STALE_VERSION,绝不静默覆盖、也绝不静默删除。改名、复制与粘贴上传不带版本闸:它们防的是另一件事 —— 目标必须不存在(改名预检,复制用 COPYFILE_EXCL、粘贴上传用 wx 让内核来判)。upload 的字节也走 guardMutation():那是补回被 node:fs 绕过的第三道闸。
边界说明:这不是内核级隔离。DSH 的文件沙箱本身是「可信代码对不可信路径」的包含性检查(官方注释原话:containment, not a security boundary*),本插件与官方 Agent 的 write 工具站在同一层能力上,没有引入更宽的口子;能访问到这个 Host 端口的本机用户,本来就能让 Agent 写文件。
与 Agent 的关系
- 插件不给模型加任何工具:Agent 自己的 read / edit / write 工具足以改文件,再加一个同名工具只会污染工具表。
- 插件不伪造文件观察状态(不 emit('fs/observed', …),也不用 replaceIfVersion 去「补」一次观察)。人在 GUI 里改过的文件,对模型来说就是「没读过」—— 模型应当重新 read 之后再 edit,这是正确且更安全的语义。
- 反向的一条:人在编辑器里保存后,Agent 那边基于旧内容发起的 edit 会照常撞上版本不符,不会把人的修改冲掉。
排错
浏览器侧的现象先看状态条与横幅文案(它们直接对应下表的码);Host 侧的现象看 Harness 日志里那条 500 write-failed。
| 症状 | 原因 | 处置 |
| 页面报「Failed to load plugins … loaded without registering @dongasai/dsh-editor via __ModuleLoader__.load」,插件整个不出来 | 跟别的插件撞了顶层变量名。宿主把 profile 里所有 client bundle 拼成一个 classic script(不按包隔离作用域),两个插件各有一个同名顶层 const 就会让整条 combo 抛 SyntaxError、一个都不注册 —— 而这条报错完全不提撞名(真踩过:与 @dongasai/dsh-sidebar-terminal 的顶层 const PACKAGE_NAME 相撞)。node --check 也查不出来,因为单看每个文件语法都是好的 | 跑 node reference/probes/combo-scope.mjs:它按宿主的拼法把 profile 里全部 bundle 拼起来解析,并点名是哪两个包撞名。本插件顶层只允许注释 + 一个 IIFE(见 AGENTS.md 约束 25) |
| 打开文件只有只读预览,没有「✎ 编辑」按钮 | 该文件没匹配到任何渲染器,预览头部的 actions 槽位没挂上(没有扩展名的文件是典型) | 换文件名加扩展名,或从「打开方式」下拉选「编辑器」 |
| 预览工具栏上没有「🗑 删除」按钮 | 同上:这一条也注册在 sidebar.right.tab.document.actions 槽位,槽位没挂就没有;编辑器页签里也没有(编辑器不是官方预览) | 回到该文件的预览页签再删,或用终端 / Agent 删 |
| 删除报「文件已变更,未删除」 | 版本闸生效:从你点「删除」到点「确认删除」之间文件被改过 | 看一眼是不是模型在写;确认要删就再点「仍然删除」 |
| 删除报 404 not-found | 文件已经不在了(模型 / 别的工具刚删掉,或本来就不是文件) | 页签可以直接关掉;刷新文件树 |
| 删除报 403 denied / outside-workspace | 会话是 read-only 沙箱,或目标不在该会话工作区内 | 换可写会话,或确认文件属于当前工作区 |
| 删除报「删除请求没有到达 Host」 | 请求根本没到(网络 / 页面 / 反向代理) | 文件没有被删除,重试即可;连着必现就看代理是否拦了 /workspace-editor/delete |
| 删除报「Host 侧还没有这条删除路由」 | 改过 index.js 但只刷新了页面:这条路由还没注册,静态 fallback 对 POST 回 405 | 重启 Harness;文件没有被删除 |
| 「打开方式」下拉里没有「编辑器」 | 该扩展名不在 OPTIONAL_EXTENSIONS 里 | 直接点「✎ 编辑」按钮;要长期支持就在 client.js 里加扩展名 |
| 状态条一直是「读取中…」然后转成读取失败 | 该会话 / 路径读不出来(文件被删、权限、不是文本) | 文案里有 Host 的原始说明;不是 UTF-8、含 NUL 或超过 1 MiB 的文件本来就只能只读看 |
| 保存后横幅说「磁盘上的文件在编辑期间变过」 | 版本闸生效:内容被模型或别的工具改过 | 选「加载磁盘版本」看对方改了什么,或确认要覆盖再点「覆盖保存」 |
| 保存时提示「文件已存在,没有覆盖」 | 走的是 create 模式(新建语义) | 这是刻意的:新建入口不会覆盖同名文件 |
| 保存报「保存请求没有到达 Host」 | 请求根本没到(网络 / 页面 / 反向代理) | 内容还在编辑器里,重试即可;连着必现就看代理是否拦了 /workspace-editor/write |
| 保存报 403 outside-workspace | 目标不在该会话的工作区根内(多根工作区里点开了别的根) | 确认文件属于当前会话的工作区 |
| 保存报 403 denied | 会话是 read-only 沙箱 | 换可写会话,或改会话的沙箱策略 |
| 保存报 404 unknown-session | 会话已经被这个 Host 忘掉(换了 Harness 实例、存档已清) | 重开会话再编辑 |
| 保存报 413 | 内容超过 4 MiB 字符 / 请求体超过 8 MiB | 用别的工具改大文件 |
| 引导页里没有「文件编辑」卡片 | 页签类型没注册(Host 半边没重启 / bundle 是旧的) | 刷新页面;改了 index.js 就重启 Harness |
| 「文件编辑」面板里列表出不来 | 这个会话还没有工作区根,或工作区读不出来 | 面板会直接说「这个会话还没有工作区根」;否则横幅里有 Host 的原始诊断 |
| 面板里新建 / 改名 / 复制 / 粘贴报「Host 侧还没有这条路由」 | 只刷新了页面,mkdir / rename / copy / upload 还没注册(静态 fallback 对 POST 回 405) | 重启 Harness |
| 在「文件编辑」面板里点图片,开的却是编辑器并报「不是 UTF-8 文本」 | 浏览器里还是旧的 bundle | 刷新页面(图片路由在 client.js 里,刷新即生效) |
| 图片预览显示「浏览器解不开这个格式」 | 那个后缀浏览器真的解不开(tiff / heic 这类),本插件不内置解码器 | 用「用编辑器打开」看源码(svg),或在系统里打开;文件本身没问题 |
| 「在终端打开」报「这个部署没有侧边栏终端」 | 终端页签类型(官方 ui-sidebar-terminal)没注册。编辑器与文件树里的这一项是同一份实现,两边表现一致 | 装 / 启用那个客户端插件后刷新页面 |
| 「在终端打开」报了,但终端停在工作区根 | 等它变成可写超时(默认 15 秒)—— 终端页签没被显示出来、或终端数量到上限时可能发生 | 看一眼那个终端页签自己的状态;直接在里面敲 cd 也一样 |
| 粘贴报「浏览器不让读剪贴板」 | 菜单 / 工具条上的「粘贴」走 navigator.clipboard.read():非安全上下文或权限被拒时读不到 | 把鼠标点进树里,按 Ctrl/Cmd+V(paste 事件不受这条限制) |
| 粘贴的文件名变成 pasted-20261016-203245.png 这样 | 菜单 / 工具条那条路走 navigator.clipboard.read(),这个 API 不给文件名,只能按 MIME 起名 | 想保住原文件名,用 Ctrl/Cmd+V 粘(clipboardData 带真名) |
| 粘贴报「剪贴板里没有文件」 | 剪贴板里只有文本 | 先在系统文件管理器里复制一个文件;纯文本粘贴不处理(那是另一个需求) |
| 粘贴一个目录报不支持 | 浏览器拿不到剪贴板里目录的内容(只有入口,没有内容) | 用拖拽或改名 / 移动来搬目录 |
| 面板里改名 / 复制报「目标已经存在」 | 目标路径已经有东西 | 换个名字;复制会自动往后编号,但列表可能已经过期 |
| 面板里删目录报「目录不是空的」 | 你点「删除」之后目录里出现了新条目(版本闸之外的兜底) | 确认条会升级成「递归删除」,再点一次 |
| 拖拽没反应 | 有些宿主环境(iframe / 触屏)不给 HTML5 拖放事件 | 用行上的铅笔按钮改名 / 移动(同一套 Host 路由) |
| 从 shell 里 curl 这条路由拿到 {"ok":false,"error":"unauthorized"} | 不是插件的错:本 profile 装了 @xgone/dsh-remote,它猴补 ctx.webServer.register,给之后注册的每条路由套一层自己的会话门禁(wrapHttp),先撞上的是它 | 带上它自己的登录 cookie,或按「怎么验证」在页面里点;浏览器本来就带着它的会话,不受影响 |
| plugin 行是 enabled: true 但页面里什么都没变 | 浏览器 bundle 是上一次加载的 | 刷新页面(link: 安装时 client.js 每次从磁盘现读);Host 半边改了则要重启 Harness |
| 改了 index.js 不生效 | Host 半边不在 HMR 监视集内,ESM 模块缓存按解析后的 URL 命中 | 重启 Harness(禁用 / 启用插件不够) |
怎么验证
命令行能覆盖的部分
静态自检:
| 检查 | 命令 | 期望 |
| Host 半边语法 | node --check index.js | 退出码 0 |
| 浏览器半边语法 | node --check client.js | 退出码 0 |
运行时注册走 Cordis 检视工具:先 cordis_inspect_list 看这台 Host 上有哪些 provider,再用
cordis_inspect_query({ platform, provider, method, input }) 查(provider / method 名字必须
来自 cordis_inspect_list 的结果,别猜)。
| 检查 | 手段 | 期望 |
| Loader 行激活 | Host provider Config.listConfigs,{ name: "@dongasai/dsh-editor" }(或按 id workspace-editor) | 目录里出现该 entry 且 Config 正常 —— 即 apply() 跑完(cordis.patch.yml 插的是 id: workspace-editor / name: '@dongasai/dsh-editor') |
| 渲染器注册 + 正文 | Client provider Slots.listSubTree,root: sidebar.right.tab.document | occupants 里出现 @dongasai/dsh-editor/text 与 .../text-option,active: true |
| 编辑器页签注册 + 正文 | 同上,root: sidebar.right.pane.tab | occupants 里出现 @dongasai/dsh-editor/tab,active: true |
| 「编辑」按钮注册 | 同上,root: sidebar.right.tab.document.actions | occupants 里出现 { id: workspace-editor, order: 5 },active: true |
| 「删除」按钮注册 | 同上,root: sidebar.right.tab.document.actions | occupants 里出现 { id: workspace-editor-delete, order: 6 },active: true(排在「编辑」后面) |
| 「文件编辑」正文注册 | Slots.listSubTree,root: sidebar.right.pane.tab | occupants 里出现 @dongasai/dsh-editor/files,active: true |
| 页签类型注册 | 「编辑」按钮的注册在 ctx.sidebarRightTabs.register(...) 之后执行且成功 | 类型注册没抛错(同一 apply 内,上一条通过即蕴含);workspace-editor 与 workspace-editor-files 两个 kind 都在 |
| 快捷键注册 | 命令面板 / 快捷键设置里找 workspace.editor.files | 默认绑定是 Ctrl/Cmd+Alt+E,没有冲突标记 |
Client provider 的查询要等页面回一次响应(Host 转发),所以页面得开着;超时说明没有活页面。
HTTP 面(不经过检视工具,直接打这条路由):
| 检查 | 手段 | 期望 |
| 信任围栏 | 无认证的 POST /workspace-editor/write、/delete、/mkdir、/rename、/copy | 都被拒(403),不会落到 SPA fallback |
| 路由存在性 | 这五个 URL 用 GET | 各自 405 + allow: POST(而不是 SPA 的 HTML) |
本机 profile 装了 @xgone/dsh-remote 时,从 shell 打这两条会先撞上它的会话门禁(返回 {"ok":false,"error":"unauthorized"})—— 那不是本插件的应答,见「排错」表最后几行。
只能在浏览器里看的
UI 与交互(按钮点下去开不开页签、状态条、两处横幅、页签怎么排布、草稿能不能活过切换)没有前端测试环境,命令行替代不了。手工走一遍:
1. .json / .yaml / .txt 默认就是编辑器:底部状态条显示「已与磁盘同步」,改一个字变「未保存的修改」且「保存」变品牌色,点「保存」或 Ctrl/Cmd+S 变「已保存」,磁盘同步变化。
2. 打开一个 .md 或 .ts(默认是官方预览),点工具栏上的「✎ 编辑」 —— 同一张页签应变成标题为「编辑 main.ts」的编辑器页签(位置不变、不新增页签),正文可编辑;该文件的编辑器页签已开着时再点只聚焦,不会开出第二个。
3. 编辑器页签里点「换行:开 / 关」应在自动换行与横向滚动间切换。
- 状态条上的路径:「已与磁盘同步」右边应跟着这个文件的相对路径(工作区内的文件是 docs/README.md 这样,不是绝对路径);鼠标悬停显示完整绝对路径, [data-workspace-editor-status-path] 的值也应是绝对路径。把右侧栏拖窄,路径应从头部被截断(…/dir/README.md)而文件名看得见;栏窄到 CSS 也装不下时它会退化成普通截尾,这时以悬停 title 与锚点值为准。打开一个工作区外的文件时这里应直接显示绝对路径。图片预览那一面(从文件树点图片)也有同样一条,锚点同名。
4. 在编辑器里改几个字(先别保存),从文件树点开另一个文件、再点回来:草稿还在。
5. 让 Agent 改同一个文件,再回来看:出现「磁盘上的文件已被改动」横幅,你的正文没被覆盖。
6. 删除:在一个明文副本上(先 cp 一份)找到预览工具栏的「🗑 删除」——
- 点第一下:按钮变「确认删除」并带「删除后无法恢复」提示,旁边多一个「取消」;点「取消」应回到「删除」,什么都不发生;不动它 8 秒应自己变回去。
- 点第二下:页签关掉,文件从磁盘和文件树里消失(ls 确认)。
- 想验版本闸:点第一下「删除」后,在终端里 touch/改一下那个文件,再点「确认删除」——应该停在「文件已变更,未删除」并给出「仍然删除」;点它才会真的删掉。
7. 「文件编辑」面板(先 cp -r 一份工作区,或在一个新建的临时目录里试):
- 在引导页的加号菜单里能找到「文件编辑」,Ctrl/Cmd+Alt+E 也能打开;面板标题是「文件编辑」。
- 面板最底部有一行插件信息(工作区文件编辑器 · v<版本>,与插件列表里的版本一致),鼠标悬停显示插件介绍;它贴在面板底边,不随树的滚动跑;换会话 / 换工作区根后仍在底部。
元素上可断言:[data-workspace-editor-files-plugin="@dongasai/dsh-editor"] 与
[data-workspace-editor-files-version](值是版本号;读不到清单时是空串)。
- 根目录列出来了,目录在前;点目录行展开,能一层层点下去;空目录显示「空目录」。
- 单击文件 → 同一栏开了编辑器页签,面板没有消失;单击图片(png / jpg / svg)应开图片预览页签:图片显示出来、工具条上有字节数与 宽×高、「原始大小」开关能切、右键有「复制路径 / 复制相对路径 / 在终端打开」三项;再点 .tiff(或随便把扩展名改成 .tiff 的文本)应看到「浏览器解不开这个格式」+「重试」「用编辑器打开」。
- 新建文件 / 新建文件夹:工具条上的按钮落在当前选中的目录;输入名字回车,文件真的出现在磁盘上(ls 确认),面板自己也刷出来了。
- 改名:四个入口都要试一次 —— 行尾「⋯」菜单、右键菜单、工具条铅笔(先点一行再点它)、行上悬停出现的铅笔;键盘 F2 也可以。回车生效;输入 sub/name.md 会搬进子目录。
- 创建副本:文件行上的复制按钮生成「xxx 副本」;再点一次是「xxx 副本 2」。
- 粘贴系统剪贴板里的文件:在系统文件管理器里复制一个文件(图片最好,能顺带验二进制),回到面板,鼠标点进树里按 Ctrl/Cmd+V —— 文件出现在当前选中目录(选中文件时是它所在的目录),ls 能看到它,字节与源文件一致(cmp)。面板列表自己刷出新行。
重名再贴一次应变成「xxx 2.ext」。把复制的是目录的情况也试一下:应明确说「目录不支持」,不产生 0 字节文件。
- 菜单 / 工具条上的「粘贴」也贴同一份剪贴板里的文件(这条路读不到文件名,会按 MIME 起名成 pasted-<时间戳>.png);浏览器不给读剪贴板时应提示「按 Ctrl/Cmd+V」。
- 内部剪贴板:文件行「⋯」→「复制到剪贴板」,面板底部出现剪贴板行(data-workspace-editor-files-clipboard,带 data-workspace-editor-files-clipboard-mode="copy");再到某个目录行「⋯」→「粘贴」,该目录里出现同名文件,内容一致;再贴一次自动编号。换成「剪切」再粘进另一个目录 = 移动 (原位置消失),贴完剪贴板行自己消失。「清空」按钮能清掉剪贴板行。
- 键盘:点一行后按 Ctrl/Cmd+C / Ctrl/Cmd+X 应出现剪贴板行(目录上 Ctrl+C 不生效, Ctrl+X 生效);F2 仍是改名。
- 在终端打开:在子目录行右键 →「在终端打开」→ 右侧栏出现终端页签,等它起来后应当看到一行 cd '<该子目录>',提示符就在那个目录里(pwd 确认);对文件行做同样的事,应当 cd 到它所在的目录。终端里那条 cd 是插件替你敲的,属于预期。
边界:终端页签类型没注册的部署会直接说明;等 15 秒还没可写会亮「没能自动切到该目录」的横幅(终端本身已经开着);终端数量到上限(默认 8)时终端页签自己会报错,插件不叠第二条横幅。
- 复制路径:右键(或「⋯」)任意一行 →「复制路径」→ 面板上方出现「已复制:<绝对路径>」的提示,粘到终端里应当正好是这个条目的绝对路径;「复制相对路径」粘出来是相对工作区根的路径(文件在根目录下就是 a.txt,在子目录里就是 sub/a.txt)。提示几秒后自己消失,
右边的「取消」也能立刻点掉。断言锚点:[data-workspace-editor-files-notice]
(data-tone 是 ok)与 [data-workspace-editor-files-notice-dismiss]。
浏览器不给写剪贴板时(非安全上下文 / 权限被拒)应当亮错误横幅,而不是装作复制成功。
- 重挂:复制到剪贴板后切走再切回(关掉面板重开)剪贴板行仍在 —— 它是模块级状态,不是组件的。
- 拖拽:把文件行拖到另一个目录行上松手,文件真的移动了;把目录拖进它自己的子树应无反应。
- 删除:点垃圾桶 → 确认条 → 再点一次,文件消失;对非空目录按钮应显示「递归删除」,对空目录显示「删除」。想验版本闸:点「删除」后先在终端 touch 那个文件,再点确认 ——确认条应改说「变过」,按钮变「仍然删除」。
- 排序:工具条最右的排序按钮([data-workspace-editor-files-sort],值是当前键)点开有「按名称 / 按类型 / 目录优先」;切到「按类型」后,同一个目录里 b.md 应排在 a.txt 前面(默认按名字时相反)。切换顺序不应触发任何读盘(Network 里没有新的 workspaceFiles/list)。
选完一项菜单自己收起([data-workspace-editor-files-sort-menu] 消失)。
- 目录链折叠:造一条 a/b/c 只有单子目录的链(mkdir -p a/b/c && touch a/b/c/x.txt,
再在 a/b/c 里多放一个目录),面板里应当只看到一行 a/b/c,属性 [data-workspace-editor-files-row="/…/a/b/c"] 带 data-chain="b/c";点它展开的是 c 的内容。
再在 a 里放第二个目录,链应当立刻散开(a 自己一行、b 自己一行)。
- 隐藏项淡显:放 .hidden.txt,它应比同级条目淡(data-hidden="true"),而且仍然可见(这是淡显不是过滤)。
- 多选与批量条:每行左侧有一颗常显勾选框 → 点一颗,该行变实心 ✓、行背景加深、左侧多一条色带(三重信号,皮肤改不掉),工具条下方同时出现批量条
([data-workspace-editor-files-batch="1"]);点勾选框既不展开目录也不打开文件页签。
再点一颗 → 计数 2;点已勾那颗 → 取消。批量条最左那颗是全选框:半选画 −、全选画 ✓,点一下全选可见行、再点清空。Ctrl/Cmd+点 与点勾选框等价;Shift+点 取范围;Ctrl/Cmd+A 全选;Esc 或点树里的空白处清空。普通单击(不带修饰键)仍是老语义:打开文件 / 展开目录,并且不留下勾选(光标行只有一圈虚线,data-anchor="true")。右键也不清空已勾的项。
- 批量删除:选中两个文件(先 cp 备份)→ 批量条「删除」→ 确认条 → 再点一次,两个文件都消失(ls 确认),批量条与确认条都收起。部分失败:选中两个,点确认前在终端 touch 其中一个 → 确认后成功那条消失、被改过那条仍在选择里,并出现带「重试删除」的横幅
([data-workspace-editor-files-batch-failed] / -batch-retry);直接点「重试删除」应能把它删掉。
选一个非空目录加它里面的一个文件一起删:确认条应只报 1 项(子项被父目录覆盖),且请求里带 recursive。
8. 发送到 DSH 输入框(在编辑器正文里右键):
- 菜单应是「剪切 / 复制 / 粘贴 / 全选 + 分隔线 + 发送到 DSH 输入框」;原生文本框菜单被这份菜单替代(四项编辑操作照能用)。
- 选区跨 15–20 行时右键,菜单项标题应是 @<文件>#L15-20;点下去,对话输入框里出现 @<文件>#L15-20 (末尾一个空格),且 @ 文件菜单没有被唤出来。
- 只把光标放在某一行(无选区)再发,应插入 @<文件>#L<该行>;工作区内的文件应是相对路径。
- 菜单里的「剪切 / 复制」真的好用(粘到别处验证);「粘贴」把剪贴板内容插进选区。
- 第二条分隔线后是「复制路径」「复制相对路径」「在终端打开」(作用对象是正在编辑的文件):复制路径粘出来是它的绝对路径;复制相对路径在子目录里是 sub/a.txt 这样、在根目录下就是 a.txt;「在终端打开」开出的终端最终停在它所在的目录(文件在根目录下就停在根)。
成功提示在编辑器上方([data-workspace-editor-action-notice],data-tone="ok")几秒后消失;
失败(比如终端页签类型没注册)留到用户点掉(-action-notice-dismiss)。
- 反例:输入框正忙(正在提交 / 等待响应)时发送,编辑器里应亮一条「输入框正忙或内容刚变过」的横幅,输入框里已有的内容不被破坏。
9. 语法高亮(打开一个 .js、一个 .md、一个 .css):
- 正文有颜色:注释灰、字符串绿、关键字粉、数字蓝、函数名紫;暗色主题下同样有颜色。
- 光标落在正文任意位置打字,高亮层与输入框的字始终重合(不会出现两份字、不会错行);把右侧栏拖窄让长行自动换行,仍然重合;滚到正文最底部也不跑偏。
- 选中一段文字:选区是半透明色块,底下的彩色文字仍然看得见;光标(插入符)可见。
- 中文输入法:切到拼音,边打边看 —— 拼音与候选上屏的字都要看得见(组合中的字同样来自高亮层),不会出现「输入框里一片空白」;上屏后正文照常上色。
- 元素状态可断言:document.querySelector('[data-workspace-editor-highlight]') 的 data-workspace-editor-highlight 对 .js 是 on、对 .txt 是 plain。
- 上限:yes 'const a = 1' | head -c 400000 > big.js 再打开,状态应是 over-limit,正文仍是纯文本但能编辑、能保存。
已知边界
- 只处理 UTF-8 文本:非 UTF-8、含 NUL 或超过 1 MiB 字符的文件会显示说明而不给编辑。
这是浏览器侧的编辑上限;Host 接受的上限更宽(内容 4 MiB 字符 / 请求体 8 MiB),两者别混。
- 编辑器没有行号、查找替换、多文件 —— 目前只解决「能改、能存、不丢、不覆盖」。
新建入口在「文件编辑」面板里(工具条上的「新建文件」)。
- 语法高亮是近似上色、且有体量上限:按后缀选一套规则表(不是真正的语法解析),超过 256 KiB 字符 / 2 万令牌、或浏览器没有 Custom Highlight API 时按纯文本显示。
认不出的后缀也不上色(比硬套关键字表更不容易误导)。细节见「编辑器长什么样」。
- 点「✎ 编辑」会把预览页签换成编辑器页签,不能把预览就地换成编辑器正文 —— 后者的开关在文档预览自己的 store 里,外部插件没有公开入口。想回预览:从文件树再点该文件。
- 官方文件树仍然只读(不改官方 ui-sidebar-files):改名 / 新建 / 删除要在本插件的 「文件编辑」面板里做,或在预览工具栏上删单个文件(那条只对「预览得出来」的文件生效,编辑器页签里没有)。
- 删除是永久的:没有回收站、没有软删,删掉就是 unlink / rmdir / rm -r。二次确认 + 版本闸是唯一的护栏。版本闸与落盘之间有一个毫秒级窗口(并发写正好落在里面时会删到刚写下的那一份);改名的预检与 rename(2) 之间同理。代价与理由写在 IMPLEMENTATION.md。
- 「文件编辑」面板不做的事:不复制目录、不递归新建中间目录、不做下载、没有搜索与过滤。
「移动」只有两种做法:改名输入框里写相对路径、或拖到目录行上(剪贴板粘贴也能移动,但那是「先剪切再贴」)。
- 多选只认可见行:Shift 范围选择与 Ctrl/Cmd+A 全选都以当前渲染出来的行为坐标 —— 折叠起来目录、没展开的子树都不在里面(这也是为什么排序一改,范围选择的落点就跟着变)。Esc 除了关浮层,现在也会清空选区。
- 批量删除是逐条串行的:一条一条发请求(这样「正在处理哪一条」看得见、卸载时也能立刻停),所以选很多条时会有可感的耗时;每条都带自己的版本闸,失败项留在选择里由「重试删除」接手。
- 排序偏好不持久化:它活在面板组件里,换会话 / 换工作区根重挂后回到默认(目录优先 + 按名字)。折叠与隐藏项只影响显示:隐藏项是淡显而不是过滤,面板里也没有「显示隐藏文件」开关。
- 目录链折叠会多读几层:为了判断「这一层是不是只有一个子目录」,挂载后会对可见层级里还没读过的
目录各探一次(每次挂载上限 64 个,链一旦不是唯一子目录就停)。这些预读的目录不订阅变更 ——折叠标签因此可能在别处新建 / 删除之后短暂过期,展开一次就会重新读。
- 粘贴上传只有一条路、两个上限:只处理系统剪贴板里的文件(一次最多 4 MiB / 个,超了跳过并说明,剩下的继续贴)。纯文本粘贴不处理(不 preventDefault,留给浏览器默认行为),剪贴板里的目录也只能明确说不支持 —— 浏览器拿得到目录入口、拿不到它的内容。
- 两条粘贴路读到的信息不一样:键盘 Ctrl/Cmd+V 走 paste 事件,clipboardData 里带 真文件名;菜单 / 工具条上的「粘贴」只能走 navigator.clipboard.read(),那个 API 不携带文件名,所以按 MIME 起名(pasted-<时间戳>.<扩展名>,未知类型是 .bin)。后者还需要安全上下文(http://127.0.0.1 算安全上下文)与剪贴板读权限。
- 「在终端打开」会往你的 shell 里发一次输入:目录只能靠开完再敲 cd '<绝对路径>' 带过去(官方终端接口没有 cwd,见上文)。开的是全新的空 shell,但理论上仍与你手速有毫秒级竞争。
引号用 POSIX 单引号(' → '\''):POSIX shell 与 PowerShell 都认,cmd.exe 不认 —— 那些平台上这条自动 cd 可能不生效,手动敲即可。终端页签的开 / 关始终由用户与官方 UI 决定,插件不关终端、也不改它的标题。
- 图片预览只从本插件的入口进:官方文件树 / 官方「打开方式」里的图片仍然走官方渲染器(本插件注册的是页签类型而不是文档渲染器,刻意不去顶掉官方那个)。想看本插件的图片预览,就从「文件编辑」面板点图片。
- 图片预览不做解码兜底:tiff / heic 这类浏览器解不开的格式只给一句说明(插件不引入 wasm 解码器,那是另一个量级的东西);svg 想看源码用预览里的「用编辑器打开」。
- 图片是整份读进内存再交给 (Blob + 对象 URL,换文件 / 关页签时释放),所以超大图和编辑器一样受 Host 读接口的体量约束。
- 「复制相对路径」是相对工作区根算的,不是相对当前展开的目录;工作区根自己没有单独的行 (根只出现在工具条那行路径里),所以这两个动作都作用在条目上。
- 剪贴板(内部那份)只活在内存里:刷新页面、换个浏览器标签就没了;它记的是绝对路径,换会话 / 换工作区后粘贴会被 Host 的包含性检查挡下来(403 outside-workspace),不会贴错地方。
- 面板的版本闸只盖到「直接条目」:删目录时比对的是目录自身的版本令牌(它随直接子项的增删变化),子目录内部的改动不会改变它 —— 递归删除的残余风险见 IMPLEMENTATION.md。
- 面板不会去动已经开着的编辑器页签:在面板里改名 / 移动 / 删除一个正被编辑器打开的文件时,那张页签还指着旧路径(保存会按版本闸报错或把它重新创建出来)。改名后想接着编辑,从面板里再点一次那个文件。
- 拖拽依赖 HTML5 拖放事件:宿主不给这些事件时(部分触屏 / 嵌入环境)拖不动,用铅笔按钮改名一样能移动。
- link: 安装时 Host 半边改完要重启 Harness:本目录不在 HMR 的监视集内, plugin_manager 的禁用 / 启用只会重挂 fiber,不会重新 import() 模块。浏览器半边不受这条限制(bundle 按内容 revision 取,刷新即新)。
- 草稿只在浏览器内存里:刷新页面、换个标签页打开同一个会话都会丢未保存的草稿(它是「重挂活下来」的缓存,不是持久化)。
- 一次保存是一个请求:没有分片、没有断点续传,超过上限的文件只能换工具改。
- 「发送到 DSH 输入框」发送的是纯文本,不是文件芯片:官方公开面只提供 insertText,带结构化引用的插入不跨插件边界,所以 @路径#L行号 以普通字符落进输入框(末尾带一个空格),行号语义由模型侧解析。设计取舍见 IMPLEMENTATION.md。
- 发送目标恒为「当前会话」的输入框:写入面取「编辑器自己槽位上的 inputActions」或「conversation.input.dock 哨兵登记的那一份」。两者都没有时(会话还在起、对话区没打开、输入框被审批 / 问答接管)这项不可用,编辑器里会亮横幅说明。
- 右键菜单替代了原生文本框菜单:剪切 / 复制 / 粘贴 / 全选都在菜单里,但「粘贴」依赖 navigator.clipboard.readText —— 非安全上下文(用局域网 IP 的 http 打开页面)或权限被拒时菜单里的粘贴不可用,键盘 Ctrl/Cmd+V 不受影响。
- 插入不会替你聚焦输入框:文本落在输入框里光标上次所在的位置,要接着说事请自己点一下输入框(公开面没有「聚焦」这个动作)。
相关文件
| 文件 | 关系 |
| IMPLEMENTATION.md | 为什么这么实现:设计决策、两半怎么拼、四条约束与「文件编辑」面板的来龙去脉 |
| AGENTS.md | 给代理的速查:命令、硬性约束、改动连带项 |
| index.js | Host 半边全部实现(六条路由) |
| client.js | 浏览器半边全部实现(编辑器 + 「文件编辑」面板) |
| cordis.patch.yml | profile 里的插入行 |预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
dongasai/dsh_editor
在侧栏直接编辑、保存和删除工作区文件
相关插件 · 其他
模型破甲清洗HunYiSage/dsh-purge
⚔️ DeepSeek Harness 网络安全红队工具dsh-purge
★ 16,993已汉化DeepSeek 客户端
73
女娲蒸馏alchaincyf/nuwa-skill
输入名人名字,提炼其思维模型与决策方式供你调用
★ 37,308已汉化DSH
72
逆向分析桥接Fanqi89/dsh-rea
REA(Reverse Engineer Anything)MCP 桥接,用来把 REA 逆向工具接入 DSH 插件
★ 51已汉化DSH
72
语音女友beiyege-01/dsh-voice-ai-girlfriend
语音 AI 女友Voice AI girlfriend for DeepSeek Harness:Whisper…
★ 888已汉化DSH
71.6
红队越狱测试套件Minglink/dsh-infinite-gen-5
DeepSeek v4.1 flash 逆向工具无限五代 求 Star 收藏 ⭐欢迎大家提交项目的改进
★ 3,759已汉化DSH
71.4
协同办公空间dream-num/univer-workspace
一个让人类与 AI Agent 共同创作、协作和审阅的开源 Office 工作空间。
★ 2,269已汉化DSH
70.8
这个插件是你维护的吗?
作者本人可认领并核对本条目;认领通过后,详情页会显示「作者已认领」标记。