# dsh-drop-any-file · 让 DeepSeek Harness (DSH) Web 聊天支持拖拽任意类型文件 > 扩展 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) Web UI 聊天的拖拽能力,让**任意**文件类型都能被接受——内置输入框只允许图片(PNG/JPG/WebP/GIF)内联附加,其余一律拒绝。拖入非图片文件将保存到当前会话工作区,供 Agent 读取使用。 > > English: [README.md](README.md) · LLM 索引: [llms.txt](llms.txt) · Agent 指南: [AGENTS.md](AGENTS.md) ![dsh-plugin](https://img.shields.io/badge/dsh--plugin-ready-4c8dff) ![DeepSeek Harness](https://img.shields.io/badge/DeepSeek%20Harness-✓-0f1115) ![license](https://img.shields.io/badge/license-MIT-green) ![install](https://img.shields.io/badge/dsh%20plugin%20add-✓-22c55e) **关键词 / Keywords**: `dsh-plugin` · `deepseek-harness-plugin` · drag-drop · file-upload · workspace · attachment · any-file · 拖拽 · 任意文件 · 工作区 ## 仓库 / Repo GitHub: [Zenjibad/dsh-drop-any-file](https://github.com/Zenjibad/dsh-drop-any-file) · 需要 DSH ≥ 0.1 及 web profile。 --- ## 📑 目录 - [✨ 特性](#-特性) - [🏗️ 工作原理](#️-工作原理) - [🚀 快速开始](#-快速开始) - [⚙️ 配置](#️-配置) - [❓ 常见问题](#-常见问题) - [⚠️ 安全须知](#️-安全须知) - [📦 项目结构](#-项目结构) - [🙏 致谢](#-致谢) --- ## ✨ 特性 | 特性 | 说明 | | --- | --- | | 📥 **任意文件类型** | 拖入 `.pdf`、`.zip`、`.txt`、`.csv`、`.bin`…… 不再出现「仅支持 PNG/JPG」的拒绝提示 | | 💾 **保存到工作区** | 每个拖入的非图片文件都会写入**当前会话的工作区目录**,Agent 可用常规文件工具读取/搜索/使用 | | 🖼️ **图片行为不变** | 只包含图片的拖拽仍走内置内联附加流程——已有的好用功能不受影响 | | 🚫 **绕过输入框拒绝** | 在 window 上以捕获阶段监听拖拽事件,先于输入框的仅图片处理器执行,拒绝 toast 不会触发 | | 🫳 **拖拽遮罩 + 提示** | 拖拽时显示全窗口主题遮罩(「松手保存到工作区」);保存后用 toast 提示结果(成功或失败原因) | | 🛡️ **文件名清洗 + 大小上限** | 文件名取 basename 并剔除非法路径字符;请求体 80 MB / 文件 60 MB 上限 | | 🌗 **主题自适应** | 遮罩与 toast 全部使用 `--dsw-alias-*` 设计令牌,亮/暗色自动跟随 | | ♨️ **重启常驻** | 真实 profile 打包插件:`dsh plugin add` 安装一次,每次 DSH 启动自动加载 —— 无需 cordis_define、无需每次重装 | ## 🏗️ 工作原理 ``` 用户把非图片文件拖入聊天窗口 │ Client 半区(浏览器) ▼ └─ window 上捕获阶段监听:dragenter / dragover / dragleave / drop (从 conversation.input.dock 席位注册,该席位携带 sessionId) └─ drop 含非图片文件?→ preventDefault + stopPropagation (输入框的仅图片处理器根本收不到该事件) └─ FileReader → base64 → fetch POST /drop-any-file/api body: { sessionId, fileName, base64 } │ Host 半区(DSH 进程内) ▼ └─ webServer 路由 POST /drop-any-file/api └─ 解析目标目录:sessions.get(sessionId).header.cwd(即工作区) └─ 清洗 basename → 解码 base64 → node:fs writeFile └─ 返回 { ok, saved: { name, path, bytes } } │ Client 半区(浏览器) ▼ └─ toast:「已保存到工作区:<文件名>」(或错误信息) ``` - **浏览器主动拉取/提交**:无推送、无事件;只有用户真正放下文件时才 POST,Host 一律返回 JSON(失败时 `{ok:false,error}`,绝无非 JSON 500)。 - **纯图片拖拽放行**:处理器提前 return、不 `stopPropagation`,内置的内联附加流程照常运行。 - **持久化**:随包声明 `dsh.bundle`(`cordis.patch.yml`)+ `dsh.client`(`exports["./client"]` 打包产物),作为真实 profile 插件安装,DSH client-modules 每次启动都会扫描加载。 ## 🚀 快速开始 ### 标准安装:`dsh plugin add`(重启常驻) 从本仓库安装: ```bash # 本地目录(在本仓库父目录执行): dsh plugin --profile web add ./dsh-drop-any-file # 或直接从 GitHub(任意 DSH 机器): dsh plugin --profile web add github:Zenjibad/dsh-drop-any-file # 或: dsh plugin --profile web add git+https://github.com/Zenjibad/dsh-drop-any-file.git ``` `dsh plugin add` = 向 profile 做 pnpm add + `dsh.profile.bundles` 协调:识别到本包的 `dsh.bundle` 声明后,把 `dsh-drop-any-file` 追加进 bundle 栈。**重启 DSH,然后硬刷新浏览器标签页**(`Ctrl+F5`)。启动时 client-modules 扫描器解析 `exports["./client"]`,拖拽处理器随即生效。无需 cordis_define,重启后依旧。 > ⚠️ **注意**:安装(或更新)客户端插件后必须**硬刷新页面**(`Ctrl+F5`)——DSH 客户端 HMR 只会热替换已加载的 bundle,不会把*新增*的 bundle 注入已打开的标签页。 ### 手动挂载(备选) 1. `git clone https://github.com/Zenjibad/dsh-drop-any-file.git`(任意位置)。 2. 在 `~/.dsh/profiles/web/package.json` 的 `dependencies` 加 `"dsh-drop-any-file": "link:<仓库路径>"`,然后在 profile 目录 `pnpm install`。 3. 重启 DSH。 ### 使用前提 - 任意处于活动状态的 DSH web 会话(保存目标是当前会话的 `cwd`)。 - 没有打开会话 → 没有可保存的位置 → 插件不生效(输入框保持默认行为)。 ## ⚙️ 配置 无配置文件、无持久化设置。行为由源码中的常量固定: | 可调项 | 位置 | 默认值 | | --- | --- | --- | | POST 请求体上限(base64) | `src/index.ts` 中的 `MAX_BODY_BYTES` | 80 MB(约 60 MB 文件) | | 单个文件上限 | `src/index.ts` 中的 `MAX_FILE_BYTES` | 60 MB | | 保存目标 | 每次 drop 时解析 | `sessions.get(sessionId).header.cwd`(会话工作区) | | 拖拽拦截 | `src/client/index.tsx` | 在 `window` 上捕获阶段监听 `dragenter/dragover/dragleave/drop` | | Toast 时长 | `src/client/index.tsx` | 6 秒 | ## ❓ 常见问题 **Q: 我拖了文件还是提示「仅支持 PNG、JPG、WebP、GIF 格式的图片」?** A: 插件没有加载到当前页面。先重启 DSH(如果 Host 半区尚未挂载),再**硬刷新浏览器标签页**(`Ctrl+F5`)。新增的客户端 bundle 只有整页刷新才会加载——HMR 不会把新 bundle 加进已打开的标签页。 **Q: 我拖了文件但没反应 / 没有 toast?** A: 拖拽必须发生在聊天窗口上方、且已打开会话,并且 drop 中至少要有一个非图片文件。如果全是图片,则走内置附加流程(设计如此)。可查看浏览器控制台的 `drop-any-file save error` 日志。 **Q: 文件保存到哪里?** A: **当前会话的工作区目录**(`sessions.get(sessionId).header.cwd`)——也就是 Agent 的工作目录,Agent 可以用 `read`/`glob`/`grep` 等工具直接读取。 **Q: 支持一次拖多个文件吗?** A: 支持——drop 中每个非图片文件都会按顺序保存,toast 会逐一列出保存的文件名并统计失败数。 **Q: 能拖超过上限的大文件吗?** A: 不能——Host 对超过 60 MB 的文件返回 `413`,客户端显示失败 toast。如需更大文件,改 `src/index.ts` 里的 `MAX_FILE_BYTES`(以及 `MAX_BODY_BYTES`)后重新构建。 **Q: 装了本插件后旧的动态插件还在?** A: 本插件是打包式而非动态——没有动态孪生。若你之前安装过动态拖拽处理器,请用 `cordis_stop` / `cordis_undefine` 停掉,避免双重拦截。 **Q: 如何彻底移除?** A: `dsh plugin --profile web rm dsh-drop-any-file`(或删除 profile 依赖与 bundle 条目)后重启 DSH。 ## ⚠️ 安全须知 - **防路径穿越**:文件名先取 `path.basename()`,再剔除所有非法路径字符(`<>:"/\\|?*` 与控制字符),恶意文件名也无法逃出工作区。 - **写入范围受限**:文件只写入解析出的会话 `cwd`,绝不写入任意路径。 - **大小上限**:请求体上限 80 MB、解码后文件上限 60 MB,Host 不会缓冲无界载荷。 - **无额外 I/O**:插件不发起网络请求、不执行 shell、不扫描磁盘——只做请求的那一次文件写入。 - **同源 base64 传输**:文件字节以 base64 JSON 提交到同源路由(DSH webServer),不经过任何第三方端点。 ## 📦 项目结构 ``` dsh-drop-any-file/ ├── src/ │ ├── index.ts # host 半区:body 读取、会话 cwd 解析、文件名清洗、writeFile、/drop-any-file/api 路由 │ └── client/index.tsx # client 包:捕获阶段拖拽监听、遮罩、toast、POST 到 host 路由 ├── cordis.patch.yml # dsh.bundle patch(启动时插入插件行) ├── tsdown.config.ts # 打包 host(node ESM)+ client(CJS ModuleLoader) ├── package.json # name、exports["./client"]、dsh.client + dsh.bundle ├── lib/ # 构建产物(index.js、client.js) ├── AGENTS.md # AI agent 仓库指南 ├── llms.txt / llms-full.txt ├── README.md / README.zh.md └── LICENSE ``` ## 🙏 致谢 - [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — DSH 插件/动态运行时、Slots、主题、webServer、client-modules。 - [headroom-stats-plugin](https://github.com/Zenjibad/headroom-stats-plugin) — 打包式 client 插件构建模式参考(tsdown host/client 拆分、`cordis.patch.yml`、`dsh.client`)。 - [DSH-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) — 打包式 client 插件构建模式的另一参考。 ## 📄 License [MIT](LICENSE)