dsh-files-plus
为 DeepSeek Harness Web 客户端右侧「工作区文件」树提供完整的右键文件操作,并新增复制 / 剪切 / 粘贴与键盘快捷键。
独立插件,不修改
dsh-menu;纯 Host 路由 + 纯前端 React 组件,无第三方依赖、无构建步骤。GitHub: xuehun2/dsh-files-plus
功能
在右侧栏「工作区文件」树中右键任意文件、文件夹或文件树空白区域:
| 菜单项 | 快捷键 | 说明 |
|---|---|---|
| 粘贴到文件夹 / 所在文件夹 | Ctrl+V / ⌘V |
目录行粘贴到该目录;文件行粘贴到其父目录;无选中时粘贴到文件树根目录 |
| 复制 | Ctrl+C / ⌘C |
把选中的文件/文件夹放入插件剪贴板(不写系统剪贴板) |
| 剪切 | Ctrl+X / ⌘X |
标记为移动;粘贴成功后删除源条目 |
| 复制文件路径 | — | 把绝对路径写入系统剪贴板 |
| 新建文件夹 | — | 内联输入;目录行在其内部创建,文件行在其父目录创建 |
| 重命名 | — | 内联输入,支持中文输入法组合输入、Enter 提交、Esc 取消 |
| 在文件管理器中显示 | — | 调用官方 remote.session.openWorkspacePath({ action: "reveal" }) |
| 在浏览器中打开 | — | HTML/SVG/XML 优先使用系统浏览器关联,否则使用默认应用;目录打开可浏览的 HTML 列表 |
| 在 VS Code 中打开 | — | 走宿主官方 open-in-app 目录;文件优先使用系统关联,未找到关联时打开所在文件夹 |
| 在此处打开 PowerShell | — | Windows;优先 pwsh,回退 Windows PowerShell,窗口工作目录为目标目录 |
| 在此处打开 cmd | — | Windows;窗口工作目录为目标目录 |
| 下载文件夹 / 文件 | — | 目录流式打包 ZIP;文件直接下载 |
| 删除(红色) | — | 二次确认后递归删除 |
空白区域右键(0.1.2)
在文件树空白处右键会打开“当前文件夹”菜单,不再弹浏览器原生菜单。空白区域包括:
- 文件树根区域下方;
- 某个已展开目录的子项下方;
- 当前行按钮之外的目录行内边距。
菜单内容按当前位置推断:
| 菜单项 | 说明 |
|---|---|
| 粘贴到此处 | 粘贴到当前推断出的文件夹(有剪贴板时可用) |
| 复制当前文件夹路径 | 复制该文件夹的绝对路径 |
| 新建文件夹 | 在该文件夹内创建 |
| 在此处打开 PowerShell / cmd | Windows;在该文件夹打开独立控制台窗口 |
| 在文件管理器中显示 | 打开该文件夹位置 |
| 在浏览器中打开 | 打开该文件夹的 HTML 列表 |
| 在 VS Code 中打开 | 用 VS Code 打开该文件夹 |
目录行自身的按钮右键仍是原来的行菜单(复制/剪切/重命名/删除等);只有目录展开后的空白子区域才按“当前文件夹”处理。
新增的复制 / 粘贴能力
- 单击选中:点击文件树任意行即选中,行左侧出现品牌色高亮条;右键也会先选中该行。
- 快捷键直接操作:选中文件/文件夹后按
Ctrl+C复制、Ctrl+X剪切;选中文件夹后按Ctrl+V粘贴到该文件夹,选中文件时粘贴到其父文件夹。 - 粘贴目标推断:
- 选中目录 → 粘贴进该目录;
- 选中文件 → 粘贴进文件所在目录;
- 没有选中、但焦点仍在文件树 → 粘贴到当前树根目录。
- 冲突自动保留双方:目标已存在同名条目时,自动生成
名称 - 副本、名称 - 副本 2…… 不覆盖、不丢数据。 - 剪切语义:剪切只做标记,粘贴成功后才删除源文件;粘贴失败时源文件保持不动。
- 跨会话:剪贴板记录源会话 id,可把 A 会话工作区的文件粘贴到 B 会话工作区;Host 会分别校验两端路径。
- 状态提示:底部居中 toast 显示复制、粘贴、剪切、重命名、删除等结果;粘贴期间显示常驻「正在粘贴…」提示。
交互与视觉
- 毛玻璃浮层 + 主题 token(
--dsw-alias-*),自动跟随深色 / 浅色模式。 - 菜单支持
↑↓HomeEnd导航、Enter执行、Esc关闭、Tab切换焦点;点击外部、滚动、窗口缩放会自动关闭。 - 菜单内直接显示当前剪贴板条目,可一键清除。
- 危险操作(删除)使用错误色,并需二次确认。
- 中英双语,跟随界面语言。
- 不替换宿主组件、不改宿主样式表;只通过主题 token 和
data-*属性做轻量高亮。
快捷键
| 操作 | Windows / Linux | macOS |
|---|---|---|
| 复制选中项 | Ctrl+C |
⌘C |
| 剪切选中项 | Ctrl+X |
⌘X |
| 粘贴 | Ctrl+V |
⌘V |
不会劫持快捷键的场景(避免影响正常编辑):
- 焦点在输入框、文本域、
contenteditable、role="textbox"或终端(.xterm)内; - 文件树中已有非空文本选区(此时保留浏览器原生复制);
- 菜单打开期间(菜单内输入框可正常粘贴文本);
- 最后一次鼠标交互不在文件树内,且焦点也不在文件树内。
实现上使用 window 捕获阶段监听,因此不会与浏览器默认的复制/粘贴重复触发。
提示:在文件树中单击文件会同时打开预览。若预览已经取得焦点,快捷键会按“最后一次交互是否仍在文件树内”判断;此时请先点回文件树中的行,或直接使用右键菜单里的“复制 / 粘贴”。
安装
方式 A:从本地目录安装 Bundle(推荐)
- 保持仓库目录存在(无需构建)。
- 打开 Harness 的「插件管理」→ 安装 Bundle,目标选择本仓库目录的绝对路径:
C:\Users\血魂\Documents\deepseek-harness\default-workspace\dsh-files-plus - 安装后确认 bundle 行
workspace-files-plus为启用状态。 - Host 半区若未热更新,重启 DSH 宿主进程;随后浏览器硬刷新(
Ctrl+Shift+R)。
方式 B:作为普通本地包链接
如果已配置 pnpm 与 profile,也可以由插件管理器完成安装;不要手工修改 profile 的 package.json / cordis.patch.yml。
与 dsh-menu 共存
本插件是独立实现,不修改也不依赖 dsh-menu。两者的右键入口都监听文件树行,但本插件在 window 捕获阶段处理并阻止事件继续传播,因此两者同时启用时只会弹出本插件的菜单,不会出现两个菜单。
仍建议在确认新插件工作后停用 dsh-menu,避免保留一套不再使用的 Host 路由和 UI 组件。
目录结构
dsh-files-plus/
├── index.js # Host 半区:文件操作、复制/移动、ZIP;VS Code 由官方 open-in-app 负责
├── client.js # Client 半区:右键菜单、选中态、剪贴板、快捷键
├── cordis.patch.yml # 把插件插入当前 profile
├── package.json # 包元信息、bundle/client 声明、scripts
├── locale/
│ ├── zh.json # 插件管理页中文标题/描述
│ └── en.json # 英文标题/描述
├── icon.svg # 插件图标(原创)
├── scripts/
│ └── smoke-host.mjs # Host 路由冒烟测试(真实临时文件系统)
├── README.md
├── CHANGELOG.md
├── LICENSE
└── NOTICE # 对 dsh-menu MIT 代码段落的署名
工作原理
Client 半区(client.js)
- 在
window捕获阶段监听pointerdown/contextmenu/keydown,只处理[data-files-entry]行。 - 通过
ctx.sidebarRight.commandTarget(row)解析当前会话 id;通过文件树根节点的data-files-root读取工作区根。 - 选中状态保存在插件内存中,并在行上写入
data-dshx-selected/data-dshx-cut,由注入的 CSS 绘制高亮。 - 右键菜单挂载在
shell.overlay槽位,是一个独立的 React 浮层;状态用轻量 observable store +useSyncExternalStore。 - 不依赖
@deepseek-ai/dsh-client-ui-primitives,只使用 React 和主题 token。
Host 半区(index.js)
- 通过
connection.fetch.register注册/api/workspace-files-plus/*路由。 - 每个请求都带
sessionId,Host 解析该会话的工作区根,要求路径严格位于工作区内,并做processPathFromHostPath → fs.resolve → processPath往返校验。 - 复制/移动使用
node:fs/promises的cp/rename;跨卷移动失败时回退为cp + rm。 - 目录下载是手写的流式 stored ZIP(UTF-8 文件名、CRC32、data descriptor),不落临时文件。
Host 路由
错误响应统一为 { ok: false, error: { code, message } }。
| 路由 | 方法 | 参数 | 行为 |
|---|---|---|---|
/api/workspace-files-plus/transfer |
POST | { sourceSessionId, targetSessionId, sourcePath, targetDirPath, op: "copy"|"cut", conflict: "rename"|"overwrite"|"error" } |
复制或移动一个文件/文件夹到目标目录 |
/api/workspace-files-plus/rename |
POST | { sessionId, path, name } |
重命名(仅改名,不移动) |
/api/workspace-files-plus/mkdir |
POST | { sessionId, path, name } |
在 path(必须是目录)内新建文件夹 |
/api/workspace-files-plus/delete |
POST | { sessionId, path } |
递归删除文件或目录 |
/api/workspace-files-plus/open-terminal |
POST | { sessionId, path, shell: "powershell"|"cmd" } |
在目标目录打开独立 PowerShell/cmd 窗口(Windows) |
/api/workspace-files-plus/open |
GET / HEAD | ?sessionId=&path= |
文件返回内容;目录返回 HTML 列表 |
/api/workspace-files-plus/download |
GET / HEAD | ?sessionId=&path= |
文件直接下载;目录流式 ZIP |
VS Code 与浏览器打开不再走本插件自己的 Host 路由,而是调用宿主已安装的官方能力:
- 目录 →
POST /open-in-app/open,body{ app: "vscode", path };由@deepseek-ai/dsh-host-open-in-app的目录解析器定位已安装的 VS Code。 - 文件 →
remote.session.workspacePathApplications({ path })读取系统文件关联,再用remote.session.openWorkspacePath({ path, application })打开;找不到 VS Code 关联时回退为“在 VS Code 中打开所在文件夹”。 - 浏览器 → 优先匹配系统关联中的 Edge/Chrome/Firefox 等;没有浏览器关联时用默认应用打开。HTML/SVG/XML 在默认打开失败时回退到
/open原始文件路由。
这样就不再依赖本进程的 PATH 或自建启动器,能正确找到注册表安装、非默认目录(例如 D:\软件\Microsoft VS Code)的 VS Code。
主要状态码:200 成功;400 非法参数 / 非法文件名 / 目标不是目录 / 粘贴到自身内部 / 非 Windows 请求终端;403 目标在工作区之外;404 目标不存在;409 冲突(conflict: "error");413 请求体过大;500 内部错误。
/open-terminal打开的是独立控制台窗口,不是右侧栏的终端标签;非 Windows 平台不显示这两个菜单项。
安全设计
- 工作区边界:所有路径先取会话工作区根,再做严格前缀检查(防
D:/workspace2这类前缀穿透)与双向路径往返校验。 - 拒绝越界:源路径必须在工作区内;目标目录允许是工作区根本身,但源路径不允许是根。
- 拒绝自包含:不允许把文件夹粘贴到它自己或它的子目录里。
- 不覆盖数据:默认
conflict: "rename",同名时保留双方;只有显式请求overwrite才替换。 - 符号链接:
lstat判断条目类型;fs.cp使用dereference: false+verbatimSymlinks: true,复制链接本身而不是跟随到工作区外。 - 浏览器打开隔离:
/open返回的text/html一律附加Content-Security-Policy: sandbox,避免工作区 HTML 以 GUI 同源权限执行。 - 非 ASCII 文件名:下载响应头按 RFC 6266 同时输出 ASCII fallback 与
filename*=UTF-8''…。 - 终端启动:终端路径先经过工作区校验,再作为子进程
cwd传入;不拼接 shell 命令字符串,避免路径注入。
性能与内存
本插件刻意避开了占用内存过高的实现,因此没有引入需要用户预先决策的“大内存”逻辑:
| 环节 | 实现 | 内存特征 |
|---|---|---|
| 复制 / 移动 | OS 级 fs.cp / fs.rename,流式读写 |
不把文件内容读入 JS 堆;内存不随文件大小增长 |
| 目录下载 | 流式 stored ZIP,64 KiB 缓冲 | 文件内容不进入 JS 堆;ZIP 中央目录元数据随条目数线性增长 |
| 目录列表页 | opendir 流式读取,最多渲染 2000 项 |
有界内存,不会为超大目录构建完整数组 |
| 剪贴板 | 单条 { path, name, type, sessionId, op } |
常量级 |
| 选中态 | 单个行引用 + 两个 data-* 属性 |
常量级 |
| DOM 查询 | 仅用户交互时扫描可见 [data-files-entry] |
O(可见行数),无轮询、无 MutationObserver |
| 事件监听 | 3 个 window 捕获监听 + 1 个 focusin |
常量级 |
| Toast | 单个定时器 | 常量级 |
ZIP 的中央目录必须为每个条目保留名称、CRC、偏移等元数据(约几十字节/项),这部分随条目数增长,但不随文件内容增长;极端数十万文件的目录建议改用系统打包工具。目录列表页已在 2000 项处截断并提示,避免单个超大目录撑爆页面内存。
可选但未采用的方案(需要时再启用)
- 复制进度百分比:需要在复制前递归遍历源目录、统计文件数/字节数,复制时再逐项回调。代价是额外一次全树 IO 扫描和 O(条目数) 的进度状态;对超大目录会明显变慢。当前选择“无进度、异步完成 + 忙碌提示”。
- 覆盖确认对话框:当前冲突自动重命名,最安全且零额外状态;若需要“替换 / 保留两者 / 跳过”逐项选择,需要增加一个冲突队列和对话框状态。
- 持久化剪贴板:写入
localStorage可跨刷新保留,但重启后路径可能失效,且需要处理过期清理;当前仅内存保存。 - 拖拽复制:需要接管宿主文件树的拖拽事件并维护拖拽预览,属于另一套交互,未纳入本版本。
配置
粘贴冲突策略
Client 顶部常量:
// client.js
const PASTE_CONFLICT = "rename"; // "rename" | "overwrite" | "error"
rename(默认):同名时生成名称 - 副本,保留双方。overwrite:删除目标同名条目后粘贴,有数据丢失风险。error:直接提示冲突,不执行。
Host 路由本身接受 conflict 参数,因此三种模式都已实现;修改该常量即可切换 UI 行为。
菜单顺序 / 文案
菜单项顺序写在 client.js 的 FilesPlusOverlay 中;中英文文案在文件顶部的 zh / en 字典里。两者都不依赖构建步骤。
开发
环境
- Node.js ≥ 20(使用
fs.cp、WebRequest/Response、Readable.toWeb)。 - 无 npm 依赖;不需要
pnpm install、不需要打包。
常用命令
node --check index.js
node --check client.js
node scripts/smoke-host.mjs
npm test 等价于 node scripts/smoke-host.mjs。
冒烟测试覆盖
scripts/smoke-host.mjs 使用临时工作区和最小 Cordis 上下文,直接调用已注册的 Host 路由,覆盖:
- 同目录复制自动重命名;
- 目录递归复制;
- 剪切移动并删除源;
- 冲突
error返回 409; - 冲突
rename生成副本; - 冲突
overwrite替换; - 拒绝把目录粘贴到自身;
- 拒绝工作区外源路径;
- 重命名 / 新建 / 删除;
- 目录 ZIP 下载有效;
- 目录 HTML 列表有界且带 CSP sandbox;
- HTML 文件预览带 CSP sandbox;
- 终端路由拒绝非法 shell;
- 终端路由拒绝工作区外目录(两项校验都不会真的启动终端)。
Git 工作流
仓库按标准特性分支开发:
git switch -c feat/copy-paste
# 编辑、验证
git add .
git commit -m "feat: add workspace file copy and paste"
git switch main
git merge --ff-only feat/copy-paste
提交信息使用 Conventional Commits(feat: / fix: / docs: / chore: / test:)。
已知限制
- 单选中:一次只操作一个文件/文件夹;多选批量复制尚未实现。
- 无复制进度:大目录复制只显示忙碌状态,不显示百分比(见“可选但未采用的方案”)。
- 剪贴板不持久化:浏览器刷新后清空。
- 无覆盖确认 UI:默认自动重命名;切换为
overwrite前请确认风险。 - 跨会话复制:路径按各自会话工作区校验;若源会话工作区已被移动/删除,粘贴会失败。
- 快捷键焦点:选中文件并打开预览后,焦点可能离开文件树;此时用右键菜单操作,或先点回文件树。
- 终端窗口:PowerShell/cmd 以独立控制台窗口打开,不是右侧栏的终端标签;非 Windows 平台不显示这两个菜单项。
- 视觉验证:本仓库的开发环境没有可用的浏览器自动化控制;Host 逻辑通过 14 项冒烟测试,安装后的 Host 插件与 Client
shell.overlay槽位均已确认激活,但最终视觉效果仍需在已连接的 DSH 页面中确认。
验证记录
开发环境实际执行过的检查:
node --check index.js -> OK
node --check client.js -> OK
node scripts/smoke-host.mjs -> host smoke: 14 checks passed
plugin_manager install_bundle (local path) -> application: applied
plugin_manager list_bundles -> dsh-files-plus 0.1.2 linked
Config.listConfigs (dsh-files-plus) -> Host row present, packageDir resolved
Slots.listSubTree (shell.overlay) -> workspace-files-plus occupant active
plugin_manager list_plugins -> include:workspace-files-plus active
说明:
- Host 路由使用最小 Cordis 上下文 + 真实临时工作区做了完整往返测试(复制、剪切、三种冲突模式、边界、重命名/新建/删除、ZIP、CSP)。
- 对运行中的 Web 端口直接发起未认证请求会得到
401,说明请求在认证层被拒绝,无法用这种方式替代页面内已认证连接来验证路由;因此 Host 运行态以插件 fiberactive+ 冒烟测试为准。 - VS Code / 浏览器打开改为调用宿主官方的
open-in-app路由与remote.session.openWorkspacePath;open-in-app相关插件在 profile 中均为active,但本环境没有浏览器内自动化,未做“真正弹出 VS Code / 浏览器”的端到端验证。 - 0.1.2 新增了 Host
open-terminal路由。浏览器硬刷新只会加载新的 Client 菜单;必须重启一次 DSH 宿主进程,/api/workspace-files-plus/open-terminal才会注册,终端菜单项才能成功。 - 未进行截图或 DOM 级视觉验证;请以浏览器中实际渲染为准。
许可与致谢
MIT。见 LICENSE。
Host 半区的 ZIP 写入器、内容类型表和目录列表页参考/改编自 MIT 许可的 dsh-menu,署名见 NOTICE。VS Code / 浏览器打开使用 DeepSeek Harness 自带的 open-in-app 能力。右键菜单、选中模型、剪贴板、快捷键与复制/剪切/粘贴传输路由为本仓库独立实现。
No comments yet. Be the first to write one.