dsh-session-files
Manifest validUpload arbitrary files into a DSH session workspace (uploads/<sessionId>/) and let the agent deliver result files as clickable download cards in the conversation.
Session Files — DSH 会话文件的任意文件上传
npm 包名 / 仓库名
@deepseek-ai/dsh-session-files。
让用户可以在 DSH Web 界面里给当前会话上传任意文件(不限图片),并把文件落到本会话工作区的专用目录:
<session.workspace>/uploads/<sessionId>/<文件名>
<session.workspace>:当前会话的工作目录(session cwd);uploads/:专门存放此类内容的目录(约定名,可通过代码顶部常量调整);<sessionId>/:对应该 session 的子目录。
上传成功后插件自动向该会话注入一条消息,引导 LLM 知道「哪一个目录对应本 session」并提供文件清单;会话恢复时若该目录已存在,也会再次提示模型。
特性
- 🖇 聊天输入区左侧新增「上传文件」按钮(DSH 官方回形针图标),支持多选、任意类型(文本/PDF/压缩包/音视频/二进制…)
- 📁 文件落在
<workspace>/uploads/<sessionId>/,只有用户真正上传时才创建目录(无上传则目录不存在) - 🧠 上传成功后经
agent.inject注入模型可见消息:会话上传目录绝对路径 + 相对路径约定 + 文件清单 - 🔁 会话启动/恢复时,若上传目录已存在,自动注入同等的目录提示(休眠会话也能知道该看哪里)
- 🗂 输入区上方 dock 显示已上传文件芯片:点击打开/预览,× 删除
- 📥 文件可通过浏览器直接下载(文本/图片/PDF 内联预览,其余附件下载)
- 🎁 模型提供文件下载(v0.2.0):模型调用
share_session_file工具即可把工作区内 任意文件(处理结果等)递到对话流——渲染为可点击的下载卡片(文件名/类型/大小/说明), 会话恢复后卡片依然可见 - ⚙️ 设置页可配:启用开关、检测到 dsh 自带上传时自动让位、是否显示文件 dock;展示单文件大小上限与当前让位状态
- 🚀 上传入口让位(v0.3.0):dsh 0.1.5 自带任意文件上传,输入区已有「添加附件」。本插件 默认在检测到它时隐藏自己的回形针,避免出现两个功能重叠的按钮;取消该设置即可切回本插件的 入口(两者落点不同:本插件 → 会话工作区,内置 → DSH 附件库)
- 💬 交付语义消歧(v0.3.0):把「用
share_session_file把文件发到用户浏览器(可下载)」 与「内置present在 DSH 主机桌面上打开文件」明确区分给模型,并注册为 system prompt 规则(每会话恒在)——远程访问 Web 时后者根本用不上,模型却容易误以为已经交付 - ⬇️ 新 UI 下载入口(v0.4.0):dsh 0.1.5 自带的「交付文件卡片」与右侧 Sidebar 的 「文件」浏览都只能打开/预览文件,没有把文件交给用户的手段(卡片的下拉菜单在无桌面 主机上还是禁用的)。本插件在这两处补上自己的下载链接:交付卡片「打开」右侧的 ⌄ 菜单 多一项「下载」,主机无桌面时接管那个失效的 ⌄ 位置;文件浏览的每一行右侧多一个 下载按钮(见「新 UI 下载入口」)
- 🔐 安全:sessionId 白名单 + 文件名净化;path 下载/分享严格锁定在会话工作区内
(拒绝
..、越出 cwd 的绝对路径);单文件大小上限默认 512 MiB - 🩹 空红框修复(v0.4.1):v0.4.0 的失败提示函数与 store 的
notify()同作用域同名, 函数声明提升后覆盖了后者——每次 store 更新都会在页面底部插一个空文本的红色小椭圆, 同时 store 订阅通知失效。v0.4.1 把提示函数改名showToast并加空值防线(见下) - 🛰 远程工作区可用(v0.5.0):宿主半的所有工作区读写都改走官方文件接缝
(
ctx.fs+ctx.subprocess),不再假设「会话 cwd 就是宿主上的真实目录」。 在 SSH 远程工作区会话里,上传落盘、列表、下载/预览、删除、share_session_file全都落到远端主机上;宿主没有ctx.fs的旧版本自动退回本地node:fs实现 (行为与 v0.4.1 相同)
界面
- 输入区左侧(会话存在时):回形针图标按钮,上传中显示剩余计数角标。 dsh ≥ 0.1.5 上默认让位给内置的「添加附件」,此按钮不出现(见「上传入口让位」)
- 输入区上方 dock:「已上传 N」+ 文件芯片(名称/大小/删除),以及「本会话上传目录」的绝对路径
- 对话流:模型调用
share_session_file后,该条工具调用渲染为文件卡片 (图标 + 文件名 + 类型/大小 + 说明 + 「下载」按钮),点击即保存 - 对话流·交付文件卡片(dsh ≥ 0.1.5):「打开」右侧的下拉菜单里多一项「下载」; 主机没有桌面导致该菜单整体失效时,那个 ⌄ 由本插件接管,点开是「下载 / 复制下载链接」
- 右侧 Sidebar·「文件」浏览(dsh ≥ 0.1.5):每个文件行右侧一个安静的下载按钮
- 设置:「设置 → 会话文件上传」——四个开关 + 单文件上限 + 当前让位状态与下载入口状态 (「[生效] …」/「[已让位] …」/「[已关闭] …」,状态是显式可见的,不会"按钮悄悄消失")
安装(官方方式)
前置:dsh 已安装且目标 profile(默认 web)在运行。本插件通过官方的 dsh plugin add 安装(dsh plugin 是 pnpm 的薄转发层);包内声明了 dsh.bundle.patch(自带 cordis.patch.yml),因此 plugin add 一步即可完成安装,无需手动编辑任何 profile 配置。
# 方式一:仓库脚本(自动 npm pack + dsh plugin add)
bash install.sh
# 方式二:手动执行官方命令
npm pack -y # 产物 deepseek-ai-dsh-session-files-<ver>.tgz
dsh --profile web plugin add ./deepseek-ai-dsh-session-files-<ver>.tgz
安装成功后重启 dsh(让 profile 重新组合 bundle 层),刷新浏览器即可在输入区左侧看到 📎 按钮。
验证
重启并刷新页面后:
- 单元/集成测试(两套,都零第三方依赖):
后者把会话 cwd 设成宿主上不存在的远端占位拼写,只经"远端接缝"映射到真实目录, 因此任何一处漏改的npm test # test-host.mjs(本地世界)89 项 + test-host-remote.mjs(远程世界)38 项node:fs都会立刻失败; - 打开一个会话,输入区左侧出现 📎;点它选任意文件上传,输入区上方出现「已上传 N」芯片;
- 命令行核对(把
<dsh-address>换成你实际访问 dsh 的地址):# 客户端插件已加载(应以 window.__ModuleLoader__.load( 开头) curl -s http://<dsh-address>/plugins/@deepseek-ai/dsh-session-files/client.js | head -c 80 # 配置端点 curl -s http://<dsh-address>/dsh-session-files/config # 列表端点(目录不存在时返回空列表,不创建目录) curl -s 'http://<dsh-address>/dsh-session-files/list?sessionId=<某会话id>' - 工作区核对:
`ls <workspace>/uploads/<sessionId>/`应能看到刚上传的文件;未上传过的会话不应存在uploads/<sessionId>/目录
上传入口让位(v0.3.0)
dsh 0.1.5 起自带任意文件上传(@deepseek-ai/dsh-client-file-upload,宿主服务 fileUploads,
客户端服务 fileUpload),输入区自带「添加附件」回形针。本插件若照旧渲染自己的回形针,
用户会看到两个功能重叠的按钮。
设置项 检测到 DSH 自带上传时自动让位(默认开) 控制这件事。刻意做成设置项而不是
硬编码自动判断,因为两者落点语义不同,静默切换会埋雷:
| | 本插件 | 内置(dsh ≥ 0.1.5) |
| --- | --- | --- |
| 落点 | 会话工作区 uploads/<会话ID>/ | $DSH_HOME/attachments/v1/(内容寻址) |
| 属性 | 可写;随工作区备份;文件侧栏可见 | 只读副本;不在工作区;侧栏看不到 |
| 模型拿到 | 一条说明本会话上传目录的注入消息 | 逐条 prompt 的文本 handle + 只读路径 |
探测走两条渠道取「或」,因此不会出现按钮先闪出来再消失:客户端服务(ctx.get("fileUpload"),
apply 时同步可得)与宿主 /config 的 builtinUpload(权威但异步,作为兜底)。
dsh 0.1.0-rc.x 上两条都是 false,行为与 v0.2.2 完全一致。
交付语义:「发给用户」≠「在用户电脑上打开」(v0.3.0)
内置的 present 工具(dsh 0.1.5 新增,挂在 agent preset 里)只把工作区文件声明为交付物,
它提供的动作是在 DSH 主机(服务器)的桌面上打开文件 / 在文件管理器中显示。用户远程访问
Web 时那个动作不可用(宿主在打开前就用 workspaceDesktop().available 拦掉),而模型很容易
把「我调用了 present」当成「用户已经拿到文件了」,于是不再调用 share_session_file。
share_session_file 走的是浏览器下载卡片,是远程用户真正拿到文件的唯一方式
(dsh 0.1.5 全库只有 dsh-session-log-export 用了 Content-Disposition,没有任何
download 属性或下载包;文件侧栏只是有界预览)。三处让模型知道:
share_session_file的工具描述(首句即「把文件直接发给用户」);- 注入的会话消息尾部(短句,只在真有上传文件时出现);
- system prompt section
dsh-session-files:delivery-vs-open(每会话恒在)——与官方ui-deliverables宿主半用的同一个缝;其order在版本间变过(0.1.0-rc.x 是字面量190,0.1.5 起是getSectionOrder("DELIVERABLE_FILE_REFERENCES")=9000), 因此本插件走getSectionOrder(...) + 1,旧版回退190 + 1,两代都紧跟在官方指引之后。
文案写成「若你的工具列表里有 present」的条件句式,不做存在性探测:0.1.0-rc.x 上该工具
还不存在;0.1.5 上它在 agent preset(agent plane)里,而 profile 作用域的 tools.get()
看到的是全局层视图,探测并不可靠。
新 UI 下载入口(v0.4.0)
dsh 0.1.5 的 Web UI 多了两处"看文件"的地方,两处都只有打开,没有交付:
| 新界面 | 它自带的能力 | 缺的那一半 |
| --- | --- | --- |
| 交付文件卡片 ui-deliverables(对话流每轮末尾) | 「打开」把文件在主机桌面上用默认应用打开;⌄ 菜单有「用默认应用打开」「打开所在文件夹」 | 主机没有桌面时整个菜单 disabled,⌄ 点不动;有桌面时对远程用户也没意义——两处都没有下载 |
| 右侧 Sidebar 的「文件」浏览 ui-sidebar-files | 点行 → 在 Sidebar 里预览 | 没有下载;想拿到文件只能先预览再想办法 |
本插件正是"把工作区文件交给用户浏览器"的那一半,于是在这两处补上自己的下载链接
(GET /dsh-session-files/file?sessionId=…&path=…&download=1,与 share_session_file
卡片同一个端点、同一套工作区越界校验):
- 交付卡片:能借 DSH 的 ⌄ 菜单就借——菜单打开时注入一项「下载」(克隆它自己的菜单项
节点,因此样式天然一致)。菜单因主机无桌面而
disabled(根本打不开)时,在同一位置 接管:隐藏那个失效的 ⌄,放一个外观一致的 ⌄,点开是本插件自己的小菜单 ——「下载」+「复制下载链接」。主机能力恢复(菜单可用)时自动把位置还回去。 - 文件浏览:每个文件行的右侧加一个安静的下载按钮,点击直接下载,不改变"点行打开预览" 的原行为;嵌套目录、中文/空格文件名都照常(路径经 URL 编码走同一端点)。
为什么是 DOM 注入:dsh 0.1.5 的客户端扩展点里没有"交付卡片菜单项"和"文件树行操作"
这两席(sidebar.right.tab.menu.item 只覆盖 tab 胶囊的右键菜单),而这两处恰恰是用户找
下载的地方。注入只做三件事——追加自己的节点、临时隐藏一个已经失效的按钮(可逆,
能力恢复即还原)、监听点击;不移动、不删除、不修改 DSH 自己的节点内容。观察者被节流到
4 次/秒,且只在 MutationObserver 报告变化时运行。
设置项 在新版 UI 的交付卡片 / 文件浏览里提供下载入口(默认开) 可以随时关掉:
关掉会立刻回收所有注入物(包括还原被隐藏的 ⌄),重新打开立刻恢复,不需要刷新页面。
dsh 0.1.0-rc.x 上这两个界面不存在,注入自然什么也不做,行为与 v0.3.0 一致。
已知边界
- 注入依赖 DSH 自己界面上的稳定锚点:
[data-presented-file](卡片)、卡片预览按钮的title(绝对路径)、⌄ 的aria-haspopup="menu"、文件行的li[data-files-entry=file]与data-files-path。DSH 改这些锚点时要跟着改(dsh 0.1.5-rc.1/rc.2 实测均可用)。 - 菜单项注入只在点击 ⌄ 之后发生,因此不会影响没打开菜单时的 DOM。
- 「本轮文件改动」(
produced files)那些内联文件芯片仍未提供下载入口:它们是行内文字链接, 没有下拉位置可依附,加图标会破坏阅读节奏。需要时可先用 Sidebar 预览或share_session_file。
远程工作区(SSH)支持(v0.5.0)
工作区在远端主机上时(会话 cwd 由提供者插件路由到 SSH,例如
dsh-workspace-enhancement),宿主上的 cwd 只是一个占位拼写
(<DSH_HOME>/dsw-routes/<机器id>/<远端绝对路径>),真正的文件在远端。v0.4.1 及以前,
宿主半直接 node:fs 操作 cwd —— 落到虚空,于是下载/预览报 文件不存在或不是普通文件、
上传报 文件落盘失败、列表为空。v0.5.0 把这一层整体换成官方接缝:
| 能力 | 走的接缝 |
| --- | --- |
| 解析 / 元信息 / 列目录 | ctx.fs.resolve / stat / listDir |
| 下载、预览 | ctx.fs.readByteRange(1 MiB 一窗,不整文件进内存) |
| 小文本上传 | ctx.fs.writeText |
| 二进制 / 大文件上传 | ctx.subprocess(sh -c 'cat > "$1"',流式) |
| 建上传目录、删除文件 | ctx.subprocess(mkdir -p / rm -f;接缝没有这两个能力) |
| 给模型/界面的路径 | ctx.fs.processPath = 执行世界里的绝对路径(远端会话给远端路径) |
世界完全由接缝决定(本地 / SSH / 容器 / WSL),插件不再猜;node:fs 只用于宿主侧
临时文件,以及宿主没有 ctx.fs 时的 LOCAL_FS 兜底(等价 v0.4.1 行为,也因此
inject 里刻意不写 "fs")。细节、两个实测踩坑(远端 writeText 不建父目录、
为什么不能把 fs 放进 inject)与验证矩阵见 RELEASE_NOTES-v0.5.0.md。
v0.4.1:修掉页面底部那个空的红色椭圆
v0.4.0 新增的失败提示函数写成了 function notify(text),而本文件上面 store 段里
早有一个 function notify()(通知 React 订阅者)。DSH 的客户端插件是一个 flat bundle,
factory: (require) => { … } 的函数体就是唯一作用域 —— 两个同名声明里后者覆盖前者:
function notify() { for (const fn of listeners) fn(); } // store,被覆盖
function notify(text) { …appendChild(空红框)… } // v0.4.0 toast,实际生效
后果有两条:
- 看得见:store 每次
update()(切换会话、刷新文件列表、visibilitychange、 上传进度、页面加载…)都会往document.body插一个空文本的红框 (fixed / left:50% / bottom:28px / #ec1313 / radius:10px→ 28×16 px 的红色小椭圆), 4.2 秒后自动移除; - 看不见:store 的订阅通知整个失效,
subscribe(force)的组件与注入器的scheduleSweep收不到通知,会话切换后下载入口可能持着过期的sessionId。
修法即改名 + 空值防线(宿主半一行未动):
- function notify(text) {
+ function showToast(text) {
+ if (text === undefined || text === null || text === "") return;
排查记录、复现脚本与截图:DSH/tasks/2026-09-13-dsh-red-ellipse/;
可复用教训:DSH/knowledge/dsh-client-plugin-notify-shadowing.md。
目录与模型引导
- 上传目录固定为会话 cwd 下的
uploads/<sessionId>/,模型可通过 fs 工具直接访问(相对 cwd 或绝对路径); - 上传成功后注入的模型可见消息包含:目录绝对路径、相对路径约定、本次文件清单,以及「用户提到上传的文件时,先列目录再按需读取」的指引;
- 会话启动/恢复时(
agent/session-start)若目录已存在,注入同类提示;目录不存在则不做任何事(也不会创建); - 引导消息同时告知
share_session_file工具:用户索要文件时,应调用它(而不是只报路径)。
安全说明
sessionId必须匹配[A-Za-z0-9._-]{1,80}且是当前已加载的会话;- 文件名只取叶子名,拒绝路径分隔符、控制字符、
./..、隐藏文件与超长名; - 单文件大小上限默认 512 MiB(
uploads目录在首次上传时创建,临时文件写在同一目录并以.part-前缀隔离,成功后原子 rename); - 下载端点做了类型映射与目录锁定,GET 只能读取该会话目录内的文件。
卸载(官方方式)
dsh --profile web plugin remove @deepseek-ai/dsh-session-files
随后重启 dsh 并刷新页面;plugin remove 会自动把本插件从 dsh.profile.bundles 层移除(不再加载),无需手动清理配置。已上传的文件保留在 uploads/<sessionId>/,不会被删除。
打包与发布
npm run check # ① 同作用域重名检测(含检测器自测)——prepack 会自动先跑
npm pack # ② 产物 deepseek-ai-dsh-session-files-<ver>.tgz
prepack 钩子已接上 npm run check:检测不过就打不出包。
CI 见 .github/workflows/checks.yml(push/PR 跑 node --check + npm run check,
零依赖、不需要 npm install)。
发布到 npm 前把 package.json 的 "private": true 去掉。发布到 GitHub:
仓库即本目录(不含 node_modules/);tgz 可保留在仓库中作为安装资产,
或改用 GitHub Releases 附件 + 仓库内保留 install.sh 与文档。更新发行物时
请同时新增 RELEASE_NOTES-v<ver>.md 并更新本 README 与 GITHUB_INTRO.md。
防同名覆盖:scripts/check-client-shadowing.mjs
客户端半是一个 flat bundle——window.__ModuleLoader__.load({ factory: (require) => { … } })
的函数体就是唯一作用域。在这个作用域里写两个同名 function 声明(或 function + var),
后者会静默覆盖前者:语法合法、不抛错、不打日志,node --check 与打包都不报,
只在运行时以"某个功能莫名失效 / 多出一个奇怪的 DOM 节点"的形式暴露。
v0.4.0 就栽在这上面:失败提示 notify(text) 覆盖了 store 的 notify(),
于是每次 store 更新都往页面底部插一个空的红色 toast(详见
RELEASE_NOTES-v0.4.1.md 与 DSH/knowledge/dsh-client-plugin-notify-shadowing.md)。
检测器做的是作用域级比对,而不是简单的文本 grep:
- 自己写的词法器,正确跳过注释 / 字符串 / 模板 / 正则(CSS 模板里的
{}不会打乱作用域); - 按
{}、for (…)、catch (…)建作用域树,只报同一作用域内的重名; - 区分
function声明与具名函数表达式(React.memo(function X(){})不误报)、 区分语句位置的class与class extends匿名表达式; var归到最近的函数作用域;空圆括号/大括号不配平会在报告里警示;- 内置 12 条自测(含"扁平 bundle 里两个同名 function"这一真实形态、正则/模板/块作用域
/
for…of各一条防误报用例);检测器自身不可信时直接以退出码 2 终止。
node scripts/check-client-shadowing.mjs # 缺省扫描 ./lib/*.js
node scripts/check-client-shadowing.mjs lib/client.js # 指定文件
node scripts/check-client-shadowing.mjs --self-test # 只跑自测
node scripts/check-client-shadowing.mjs --json # 机器可读
退出码:0 干净;1 发现会覆盖的重复声明;2 用法/读取/自测失败。
精度实测:对 dsh 0.1.5-rc.2 自带的 50+ 个官方 client bundle(含大量压缩代码、
具名函数表达式、class extends)零误报;对 v0.2.2 / v0.3.0 判为干净,
对 v0.4.0 精确报出 notify(第 271 行 vs 第 370 行)。
可复用副本(供其它插件仓库取用):DSH/templates/plugin-checks/。
License
Similar plugins
Upload any file to DSH, isolated per workspace and session — a composer file library with server-side thumbnails and a context menu to open, copy, re-download or delete.
★ 0
dsh plugin --profile web add dsh-upload-fileFile attachments for the dsh web GUI: toolbar upload button (multi-select) plus drag-and-paste add files to the chat input; images follow the native draft-image flow, documents are saved into the sess
★ 0
↓ 483/wk
dsh plugin --profile web add @wszhoho/dsh-file-attachmentExtends the DSH web chat drag-and-drop to accept every file type: non-image files are saved into the active session workspace so the agent can read and use them.
★ 0
dsh plugin --profile web add dsh-drop-any-fileby CocoSgt
Bring any file into the conversation — stashed to the workspace and referenced by path; zero type rejection
★ 12
JavaScript
Aug 14, 2026
dsh plugin --profile web add dsh-attachmentsby ld-1101
DSH (DeepSeek Harness) plugin: workspace file preview column with Markdown/PDF/image/CSV preview, session-produced files, drag files/folders into the conversation as plain paths.
★ 0
MIT
JavaScript
Aug 17, 2026
dsh plugin --profile web add dsh-file-previewby wackyju2-beep
更好的 DSH | Unofficial dsh plugin: archived sessions & task notifications / 已归档会话管理 · 任务系统通知
★ 4
↓ 182/wk
MIT
JavaScript
Sep 8, 2026
dsh plugin --profile web add dsh-better