dsh-drop-path
拖入本地文件时,自动把文件的电脑绝对路径填入输入框,并复制到剪贴板。
Drop a local file into DSH and its absolute filesystem path lands in the composer.
DSH 的输入框只接受图片附件,拖其它文件(.txt / .pdf / .zip / 代码……)通常会被拒绝,什么也不会发生。
本插件接管这类被拒绝的拖放:提取文件在磁盘上的真实路径,写进输入框,顺手复制到剪贴板 ——
于是可以直接把路径发给模型去读。
原理
DSH 桌面端 preload 暴露了 window.__DSH_DESKTOP_FILE_PATH__.getPathForFile(file)(一个
contextBridge 桥,内部是 Electron 的 webUtils.getPathForFile),本插件用它把操作系统拖拽的
File 对象解析成真实路径,例如 C:\work\report.pdf、/home/you/report.pdf。
插件本体是一个 DSH 服务端 bundle 插件(cordis 风格 name / inject / apply):
ctx.webServer.register 注册 /dsh-drop-path/script.js;
ctx.webServer.tapIndex 把 <script defer src="/dsh-drop-path/script.js"></script> 注入网页 index;
- 注入的脚本在
window 捕获阶段监听拖放,把路径写进当前输入框
(兼容 textarea 与 contenteditable,对 React 受控输入走原生 setter + input 事件)。
行为模式
左下角有一个小的「路径拖放」开关,点按钮循环切换或下拉选择,状态存在浏览器 localStorage:
| 模式 | 说明 |
|---|
| 自动(默认) | 非图片文件(含未知类型)→ 填入路径;纯图片拖放 → 交回 DSH 当附件处理。 |
| 总是 | 所有文件的拖放都填入路径(包括图片)。 |
| 关闭 | 不拦截,恢复 DSH 原行为。 |
控制台排障:
window.__dshDropPath.reset() // 强制清掉可能残留的拖拽浮层
window.__dshDropPath.setMode('off'|'auto'|'always') // 直接切模式
window.__dshDropPath.mode() // 读当前模式
实现要点:不要和 DSH 自带的拖放浮层打架
DSH 的 ComposerAttachments(@deepseek-ai/dsh-client-ui-attachment)在 document 上以冒泡方式监听
dragenter / dragover / dragleave / drop,dragenter 时就 setDragActive(true) 弹出「拖入图片」浮层,
而浮层只有它自己的 drop 处理或 window 的 dragend 才会关掉:
const onDragEnter = (event) => { /* 有 Files 就 */ event.preventDefault(); dragDepth.current += 1; setDragActive(true) }
const onDrop = (event) => { ...; event.preventDefault(); reset(); if (canAcceptDrop) onAddImages([...dataTransfer.files]) }
window.addEventListener('dragend', reset)
又因为它的 canAcceptDrop 只判断「编辑器是否空闲、是否支持图片」而不看文件类型,
所以拖任何文件都会弹浮层。于是有一个很容易踩的坑:如果在 document 的捕获阶段监听 drop
并 stopPropagation(),DSH 的 onDrop 就永远不会执行 → reset() 不被调用 → 浮层永久卡在屏幕上。
本插件的做法:
- 监听挂在
window 的捕获阶段(早于 document 上的任何监听)。要接管的拖放直接 stopPropagation(),
DSH 完全看不到这次拖拽 → 浮层根本不会出现,也就无从卡住。
- 开始接管时向 window 派发一次
dragend,强制 DSH 的 reset() 跑一遍,清掉可能残留的浮层;
用 syntheticDragEnd 标记避免这个自派事件误清自身状态。
drop 阶段用真实的 File.type 再确认一次:若最终是纯图片,不吞事件,交回 DSH 正常当附件收下
(DSH 的 onDrop 不依赖 dragActive,所以浮层没出现也能收下图片)。
- 1.2s 看门狗:拖拽被 Esc 取消、
dragleave 丢失时,提示条自己消失,绝不留残影。
安装
已发布到 npm 后:
dsh plugin --profile <profile> add dsh-drop-path
本地开发(把本仓库目录 link 进 profile):
dsh plugin --profile <profile> add link:<path-to-this-repo>
# 等价于在 profile 目录执行:pnpm add link:<path-to-this-repo>
手动方式:把包装进 profile 的 node_modules,并把 dsh-drop-path 加进
<DSH_HOME>/profiles/<profile>/package.json 的 dsh.profile.bundles 数组:
"dsh": {
"profile": {
"bundles": [
/* ...原有 bundle... */
"dsh-drop-path"
]
}
}
生效
bundle 层是 profile 启动时读取的。新增 bundle 需重启 DSH(或让该 profile 重新加载),
之后刷新网页即可看到左下角的「路径拖放」开关。
注意:仅改动 client/drop-path.js(客户端脚本)时不需要重启 —— 服务端路由每次请求都重新读文件
并带 Cache-Control: no-store,刷新页面即可。
测试
node test/logic.test.mjs
用最小 DOM 桩在 Node 里加载 client/drop-path.js 并驱动事件,无需浏览器。覆盖 19 条断言:
非图片 / 图片 / 混合 / 关闭 / 无 preload 桥 的接管判定、提示条显隐、剪贴板内容,
以及「拖拽被 Esc 取消时提示条必须自动消失」的看门狗行为。改客户端逻辑后请先跑它。
兼容性
- 仅桌面端有意义:依赖 DSH Desktop 的 preload 桥。拿不到桥时会自动完全不接管,不影响 DSH 原行为。
- 仅桌面端路径:普通浏览器出于安全不暴露本地路径,本插件在这种情况下自动降级。
文件结构
dsh-drop-path/
package.json # 声明 dsh.bundle.patch -> ./cordis.patch.yml
cordis.patch.yml # 把 dsh-drop-path 插入 profile 层栈
lib/index.js # 服务端:注册路由 + tapIndex 注入脚本
client/drop-path.js # 注入到页面的客户端逻辑
test/dom-harness.mjs # 最小 DOM 桩
test/logic.test.mjs # 回归测试
License
MIT