DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

dongasai /

dongasai/dsh_editor

Verified

dsh编辑器插件

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@a918ad1e

@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 万令牌就不上色(正文照旧能编辑、能存), 上色用 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 想看源码用预览里的「用编辑器打开」。
  • 图片是整份读进内存再交给 <img>(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 里的插入行
—/ 5

No ratings yet

Verified DSH bundle

Commit a918ad1e77e2

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout