dsh-file-drop
Verified@kewab/dsh-file-drop · v1.1.3 · MIT · Web UI
DeepSeek Harness plugin: drag a file or folder out of the Workspace Files tree onto the composer to insert the same @ reference the @ menu inserts.
Install
dsh plugin add @kewab/dsh-file-drop Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Published to npm without a public repository. Inspect the package contents before installing.
Tags
Creators
Readme
dsh-file-drop — 拖拽工作区文件到消息框以插入 @ 引用
给 DeepSeek Harness 的「工作区文件」面板加上拖拽引用能力:把文件树里的文件或文件夹拖到消息框上,就会在光标处插入一个与 @ 菜单同款的引用 chip(@路径),不必再手打 @ 再翻列表。
行为
- 拖动来源:右侧栏「工作区文件」树中的任意
file/directory行(other类型如 socket、设备文件不可引用,会被忽略)。 - 拖动区域就是那一行本身(也就是平时 hover 会高亮的圆角条):目录展开后拖的是该目录这一行,不会把展开出来的整棵子树一起拖起来,也不要指望在列表空白处按下就能拖。
- 鼠标样式沿用宿主:停在文件名上仍然是普通指针(宿主自己的
cursor:pointer),不额外变手型;只有在真的拖起来之后才变成抓取光标。 - 拖拽图像 = 那一行 hover 时的样子(同样的圆角、同样的 hover 底色),不再是一个直角方块;在行的哪个位置按下,图像就抓在那个位置。
- 落点:消息框(composer)卡片。悬停时卡片出现虚线高亮表示可以放下。
- 结果:在光标位置插入引用 chip,后面自动补一个空格(与键盘选择
@候选完全一致)。 - chip 图标沿用文件树:拖入文件时,chip 左侧画的就是工作区文件树里该文件名的同款类型图标(
client.js是那个彩色的.js图标,README.md、a.pdf、pic.svg各是自己的图标);目录沿用宿主自带的文件夹图标,因为这本来就是树里那个文件夹图标。页面型图标(.md、.svg、图片、PDF 之类)画的是「深色页面 + 白色标记」,插件给它上的色就是宿主给这一类文件上的颜色(.md是 deepseek-500 蓝,.svg是紫色),而不是 chip 的引用蓝——用引用蓝会把页面和标记糊成一块纯蓝色。 - 图标的寿命:图标是在插入那一刻贴到 chip 上的,正常打字、改草稿、切会话、滚消息都不会掉。撤销/重做、以及发送失败后宿主回填草稿,都会把这些 chip 重建一遍,新元素上没有插件贴的图标;插件因此跟着消息框自己的投影走,在这类重建之后再补贴一次,判定依据只有「这个文件名是拖进来的」——用户手打的
@引用不会被误伤(唯一的重叠:同名文件之前拖过一次时,之后手打的同名引用也会被补上图标;因为图标本来就只由文件名决定,补上的仍然是这个文件在树里的那张图标)。 - 路径写法遵循宿主
@语法:- 位于当前会话工作区根目录内 → 相对路径,例如
@src/index.ts; - 工作区外 → 绝对路径;
- 含空格或引号 → 加引号,例如
@"my docs/a b.md"; - 目录 → 引用文本保留结尾斜杠(宿主用它表示「这是个目录」,例如
@src/components/),但消息框里的 chip 不显示这个斜杠:chip 上只有目录名components,目录与否由 chip 的文件夹图标表示。
- 位于当前会话工作区根目录内 → 相对路径,例如
- 拖拽期间不会触发「附件上传」:本插件在捕获阶段接管事件,附件浮层看不到这次拖拽。
- 从系统 Finder 拖入文件的行为保持不变,仍然是附件上传。
安装
包名是 @kewab/dsh-file-drop。同一个字符串同时充当 bundle 名、客户端模块 id 和 Node 解析用的包名,三者必须逐字一致;改名时这四处都要改:package.json 的 name、cordis.patch.yml 的 name、client.js 里 __ModuleLoader__.load({ id }) 和 slots.register({ id })(客户端 id 对不上时,boot 图的「注册后校验」会直接拒绝这个脚本)。
普通 profile:一条命令
dsh plugin --profile <profile> add @kewab/dsh-file-drop
dsh plugin 把包装进 profile 的 node_modules,并把 bundles 条目自动写进 dsh.profile.bundles——凡是依赖清单里声明了 dsh.bundle.patch 的包都会入栈。装完重启 DeepSeek Harness。
desktop profile:手动三步
desktop profile 由 Electron 应用独占管理:dsh plugin --profile desktop … 会被明确拒绝(error: profile "desktop" is managed exclusively by the Electron application)。两条路:用应用内的插件管理器,或者按下面手动安装。
插件源码就在本目录。它通过 profile 的 bundle 列表加载,安装 = 三处改动:
~/.dsh/profiles/desktop/node_modules/@kewab/dsh-file-drop→ 指向本目录的符号链接(让 Host 能解析到这个 bundle);~/.dsh/profiles/desktop/package.json的dependencies增加"@kewab/dsh-file-drop": "file:<本目录绝对路径>"(让插件管理器能列举/卸载它);- 同一个 manifest 的
dsh.profile.bundles末尾追加"@kewab/dsh-file-drop"(让它真的启用)。
第 3 步是启用开关:只有第 1、2 步而缺第 3 步时,Host 能解析该包、插件管理器也能看到它,但 bundle 不会加载,功能不生效。
本目录提供了两个脚本做这三件事(不依赖 pnpm,也不需要 pnpm install):
./install.sh # 写入链接 + 依赖 + bundles 条目
./uninstall.sh # 撤销上述三者(应用起不来时可在终端直接执行)
从 npm 装到 desktop profile 时,先用任意包管理器把包取进 profile,再执行包内的脚本补最后一步(脚本会认出自己已经躺在 profile 的 node_modules 里,于是只补 bundles 条目,不会把链接指回自己):
cd ~/.dsh/profiles/desktop
pnpm add @kewab/dsh-file-drop # 或 npm i @kewab/dsh-file-drop
sh node_modules/@kewab/dsh-file-drop/install.sh
卸载同理:sh node_modules/@kewab/dsh-file-drop/uninstall.sh,再 pnpm remove @kewab/dsh-file-drop。
改动生效需要重启 DeepSeek Harness 应用:新的插件行要在启动时重新组合 Host/Client 图,运行中的进程不会重新读取 profile 清单。重启后(必要时刷新页面)即可使用;「设置 → 插件」里也能看到它(插件管理器按 profile 的 dependencies 列举,所以第 2 步的依赖条目是必要的,否则插件能用但不出现在列表里)。
关于安装方式,0.1.7-rc.2 上实测确认的几点:
dsh plugin --profile desktop …会被应用明确拒绝:error: profile "desktop" is managed exclusively by the Electron application。desktop profile 只能手动安装(如上)或通过应用内的插件管理器安装。- bundle 解析走 Node 自己的 node_modules 查找:只要
<profile>/node_modules/<包名>/package.json存在即可,不需要 pnpm lockfile、也不需要任何额外注册文件;符号链接会被正常跟随。 - 本包没有声明任何
@deepseek-ai/dsh*的peerDependencies,因此不会触发 profile 兼容性豁免流程;即使将来解析失败,profile 加载器也只会跳过这个 bundle 并在 stderr 记一行原因,不会导致应用起不来。 - 应用内「禁用第三方插件并恢复」一类的菜单动作会把
dsh.profile.bundles重置为默认值(同时备份cordis.patch.yml),届时第 1、3 步需要重新执行。
卸载:直接运行 ./uninstall.sh,或手动删掉上面的符号链接、dependencies 条目和 bundles 条目;安装前的 profile 清单备份在 <工作区>/.dsh-file-drop-install/desktop-package.json.orig。
启动期安全性(为什么它不会再让应用崩溃)
客户端插件的激活审计很严格:只要有一行插件没有激活,整个 web boot 就失败,表现为启动时弹窗 / ~/Library/Logs/DeepSeek Harness/crash-*-web-boot.log 里写着
web boot: 1 entry did not activate + @kewab/dsh-file-drop: pending (waiting for service: …)。
第一版安装正是踩了这一点:它把 @deepseek-ai/dsh-client-ui-conversation 错当成「服务名」放进了插件的 inject,而客户端 Cordis 里并不存在这个服务,于是这一行永远 pending,整个界面起不来。现已从三个层面消除这类风险:
- 插件的
inject是空数组,不等待任何服务;slots/locale/sessions改由ctx.inject([...], cb)在服务就绪后再取(宿主自己的插件如dsh-client-ui-sidebar-files也用这个写法)。服务缺失时本插件只是不工作,不会 pending。 dsh.client.inject(清单里的字段,仅用于客户端模块到达顺序)也已去掉:本插件的模块只require('react')(基础 seed),槽位声明本身已经处理了先后顺序,所以不需要额外边。apply()整体包在 try/catch 里,并把时间花在注册监听器上,任何意外都只留下一条console.error,绝不向外抛。- chip 图标另需三个基础模块(
@deepseek-ai/dsh-client-ui-primitives、react-dom、react-dom/client,都是宿主 web boot 的 seed 模块表里的键)。它们在第一次拖入文件时才require,并整段包在 try/catch 里:拿不到就只是不画文件类型图标(chip 保持宿主的通用引用图标),既不会 pending 也不会抛。
结论:这一行现在只会「激活但什么都不做」,不会再影响启动。
文件
| 文件 | 作用 |
|---|---|
package.json |
bundle 清单:dsh.bundle.patch + dsh.client(platform: web、immediately;不注入任何服务,也不声明模块顺序边) |
cordis.patch.yml |
插入一行插件 @kewab/dsh-file-drop |
index.js |
Host 半边:无操作,仅让该行可加载 |
client.js |
浏览器半边:拖拽监听、落点高亮、引用插入、chip 的文件类型图标 |
install.sh / uninstall.sh |
手动安装/卸载 desktop profile(链接 + 依赖 + bundles 三处改动;从 npm 装好后只需跑脚本补 bundles 条目) |
icon.svg / locale/*.json |
插件管理界面里的图标与文案 |
LICENSE |
MIT |
实现要点
dragstart(捕获、委托):文件树的行本身没有draggable,插件在鼠标碰到该行自身时补上draggable="true",随后在dragstart中读取该条目已经公开的data-files-path(绝对路径)、data-files-entry(file/directory)和行内名称,并写入私有拖拽类型application/x-dsh-file-drop。- 拖的是「行」而不是「条目」:目录展开后,
<li data-files-entry="directory">里除了行按钮还包着整棵子树,浏览器会拿整个<li>当拖拽图像——于是拖文件夹变成拖一大片直角方块。插件现在只给条目自己的行元素(:scope > button,也就是视觉上那个带border-radius: var(--dsw-radius-md)的圆角条)加draggable, 并且只在指针落在行内时才武装它(悬停在展开出来的子层或列表空白处不算);名称与路径仍从条目的data-files-*属性读取,行为不变。 - 拖拽图像 = hover 态的行:
dragstart里先给该行加上data-file-drop-dragging(CSS 用与宿主:hover相同的--dsw-alias-interactive-bg-hover底色 +cursor:grabbing画出 hover 的样子),再调用dataTransfer.setDragImage(row, clientX - rect.left, clientY - rect.top)把这一行本身交给浏览器; 浏览器是在dragstart返回之后才生成拖拽图像的,所以此时行已经是 hover 态,图像就是那个圆角条,而且抓取点就在指针按下的位置。dragend/ 窗口失焦 / 插件卸载时移除该属性。注意这里没有给[draggable="true"]写任何cursor规则,因此悬停时不会出现多余的手型。 - 四角为什么会变直角(以及修法):Blink 生成元素拖拽图像的
DraggedNodeImageBuilder(third_party/blink/renderer/core/clipboard/data_transfer.cc)是**「从最近的 stacking context 开始绘制,再裁剪到该元素本身」,源码注释写明这会把元素背后、同一 stacking context 中先绘制的内容一起画进来**。行的border-radius只是让圆角之外变成透明,而那块透明紧接着被身后的面板底色填满,于是图像变成一个直角方块——行自己的圆角背景其实是画对了的。 修法:拖拽期间给该行加isolation:isolate+transform:translateZ(0),让这一行自己成为 stacking context,Blink 的绘制就从该行开始,圆角外保持透明,图像与 hover 态完全一致(同样的圆角、同样的底色)。 dragover/drop(捕获):只有携带该私有类型的拖拽才会被接管;用会话作用域的探针组件(注册在conversation.input.overlay)找到指针下的消息框所属会话。- 插入:在该会话自己的 Cordis 作用域上派发
slash/input-insert-reference,payload 是{ source: 'reference', ref, label, appearance, clipboardText }, 其中span由消息框公开的inputActions.captureInsertion()取到(detect 坐标 + 实时draftRev),因此在有 chip 的草稿里位置也正确。ref/clipboardText用宿主@语法(目录带结尾斜杠),label单独给:目录只用条目名,不带斜杠——chip 显示的是label, 文件夹图标已经说明它是目录,斜杠在消息框里只会显得像路径尾巴。chip 的label是插入时缓存的,草稿恢复(restoreDraft)与投影($projectComposer)都从 chip 节点读回,所以这个显示不会被重新推导成带斜杠的文本。 - chip 的文件类型图标:chip 是 conversation 包里的 React 节点(
ReferenceChipNode.createDOM建的宿主<span data-composer-chip="reference">,加上ReferenceChip用 portal 渲染进去的子树)。往里面插/换 DOM 节点会被 React 的删除记录打中(removeChild抛 NotFoundError),所以插件对 chip 只做两件 React 不管理的事:在宿主元素上标一个data-file-drop-chip,并写一个自定义属性--dsh-file-drop-icon。 样式表负责把宿主自带的通用引用 glyph([data-file-drop-chip] svg)藏起来,再用::before以背景图把文件类型图标画在同一个位置——chip 内层本来就是 inline-flex,::before顶替原来那个 svg 的 flex 位置,align-self:center与 14px 尺寸都和原 glyph 一致,所以行高与文字位置不变。 图标本体来自宿主的FileTypeIcon+classifyFileType(与文件树同一套判定,所以client.js就是树里那个彩色.js图标),渲染到一棵脱离文档的 React root 后取svg.outerHTML;data URI 是独立文档,currentColor与主题变量都不会继承,因此先把主题变量烘成字面量(遇到var(...)别名就顺着链往下走),再encodeURIComponent成url("data:image/svg+xml,…")。主题变量必须从 chip 所在的子树读(getComputedStyle(chip 的父节点),退回document.body):这套调色板是主题样式表声明在body上的(body{--dsw-static-…}与body[data-ds-dark-theme]),从document.documentElement上读什么都读不到,页面标记与折角会静默用上兜底的灰字面量(#a2a4a6),表现出来就是「页面颜色对了、图标里的元素发灰」。 注意每个图标都按它自己 28px 的设计尺寸渲染:chip 只有 14px,而FileTypeIcon的尺寸参数同时决定width/height与viewBox,按 14px 渲染等于把画布缩小到 14 个单位,图形会被裁掉一大半(表现就是只剩左上角一团色块)。currentColor不改写、而是给文档根加一个color:通用页面图标用currentColor画页面本体,又用<g color="var(--dsw-static-neutral-00)">把白色留给页面上的标记(MD字样、图片的几块形状都是这么来的)。只有让「离得最近的 color 生效」、并把那个白也烘成字面量,页面本体与白色标记才能同时正确;给根加的那个颜色就是宿主给这一类文件上的颜色——宿主那句_icon_…{color:var(--dsh-file-type-icon-color,var(--dsh-file-type-default-color))}加上每个类型一条--dsh-file-type-default-color(.md/.code/.html取 deepseek-500,.svg/视频取那个紫色字面量rgb(139,118,246),PDF 红、Word 蓝、Excel 绿、PPT 橙、文件夹琥珀、其他灰)。这个颜色不抄进插件当唯一来源:先测量活页面——把渲染图标的那棵离屏容器临时挂到一枚已标记 chip 旁边(只有进了消息框,宿主那句 glyph 规则才生效),让宿主自己渲染一遍、读getComputedStyle(svg).color,随即摘掉;只有量不到时才退回插件里那张同样内容的常量表,因此自定义主题或--dsh-file-type-icon-color覆盖也能跟上。语言类彩色图标(CodeFileIcon那批)全是字面色,既没有currentColor也没有主题变量,根本不需要这个颜色,只走一遍变量烘焙即可。同一类型只渲染一次并缓存。 chip 何时进 DOM 由 React 决定(可能是异步提交),因此用宿主投影state.getSnapshot().occurrences在派发前后做差分,定位到这一枚 chip——同一条引用已经在草稿里时也一样,差分给出的是新插入的那一枚,而不是更早的那一枚;只有连派发前的投影都读不到时才退回「最后一枚同引用 chip」,而且位置对了还要用 DOM 上的textContent复核 label。最多 30 次 × 16ms 重试,仍对不上就安静放弃(chip 保持宿主默认样子),绝不去改别的 chip。 - 重建后的补贴:撤销/重做与发送失败的回填都会把 chip 换成新元素(标记长在元素上,于是随之消失)。插件在第一次成功拖入时订阅消息框自己的
shell.state(失效的 chip 用isConnected从账本里摘掉,避免泄漏),投影一变就防抖 120ms 扫一遍卡片上的 chip:只看 label,只要这个文件名是拖进来的就补上图标。不按「投影里的引用」来配对是有原因的:回填草稿走的是纯文本,重建出来的 chip 只有 label、没有引用,而 label 本来就是选图标要用的东西;反过来,手打的@引用从没进过这张名单,所以永远不会被补上图标。 - 所有监听器、样式与登记都通过
ctx.effect/React.useEffect注册并清理,插件热重载不会留下残留。 - 激活契约:
apply()不做任何可能抛出的服务访问,exports.inject为空数组,所需服务全部走ctx.inject(['slots','locale','sessions'], …);slots/locale在服务到位前分别退化为「不注册」和「空字符串」(translate/sessions两个闭包变量),因此该行在任何环境里都是「激活」而不是「pending」。
发布到 npm(维护者)
npm login --registry=https://registry.npmjs.org/ # 这台机器上 registry 默认指向镜像,发布必须显式指定官方源
npm pack --dry-run # 先看 tarball 里到底有哪些文件
npm publish # 走 package.json 里的 publishConfig(官方源 + public)
要点:
private必须为false/缺省,否则npm publish直接拒绝。本包已移除该字段。- scope 包默认按私有发布,
publishConfig.access: "public"已写死为公开,免去--access public。 files白名单决定 tarball 内容;README.md/LICENSE/package.json无论如何都会包含。- 发布后校验:
npm view @kewab/dsh-file-drop version、curl -sI https://unpkg.com/@kewab/dsh-file-drop/client.js。 - 升版本:
npm version patch|minor|major,再npm publish。npm 不允许覆盖已发布的版本号。