Skip to content
dsh.fish
Bundle

dsh-utility-tools

DSH 对话工具插件:拖拽任意文件附件(发送时暂存系统临时目录)+ 选中文本引用。支持静态挂载与动态加载。

Source
sharkymew
stars
2 stars
License
MIT
Updated
Updated yesterday

Readme

# dsh-utility-tools

DSH(DeepSeek Harness)对话工具插件:**拖拽任意文件进入对话** + **选中文本引用**。

支持**两种加载方式**:静态挂载(`dsh plugin add` 一行命令安装,推荐)与动态加载(`cordis_define`,零依赖)。

---

## 功能

### 1. 拖拽文件附件(不限于图片)

- 拖入任意文件到对话区域即进入**附件列表**:输入框上方显示「📎 附件(N)」胶囊;
- 点击胶囊展开**管理菜单**:查看文件名/大小、逐个移除;
- 发送(Enter 或发送按钮)时随消息一并提交:
  - **Electron 环境**:直接引用 `File.path` 原路径,不复制;
  - **普通浏览器**:字节**无损**暂存到系统临时目录(macOS/Linux 用 `/tmp`,Windows 用每用户 `%TEMP%`)下的 `dsh-drops/<会话id>/`,消息引用绝对路径,模型可直接读取;
- 文件**内容不会**出现在输入框或消息气泡里(只引用路径与大小),**不污染工作目录**;
- 图片拖拽仍走系统原生流程;图片与文件混拖时图片自动转交原输入栏。

### 2. 选中文本引用

- 在对话中鼠标选中文字 → 选区上方弹出「引用」菜单;
- 点击后选中文字显示在输入框上方(引用条),带「✕」取消;
- 发送时引用以 `> 引用:` 块置于消息开头一并提交。

---

## 目录结构

```
dsh-utility-tools/
├── README.md            # 本文件(含导入指南)
├── package.json         # 包声明:dsh.bundle(自动插入 entry)+ dsh.client(静态 Client 半区)
├── cordis.patch.yml     # ★ bundle patch:`- insert:` 插入插件 entry(一行命令生效的关键)
├── LICENSE              # MIT
├── .gitignore
├── lib/                 # ★ 静态版(一行命令安装用)
│   ├── index.js         #   Host 入口(ESM,export default 插件,零依赖)
│   └── client.js        #   Client 半区(__ModuleLoader__.load 格式)
└── dynamic/             # 动态版(cordis_define 粘贴用,已验证)
    ├── host.body.js     #   code.host 函数体
    └── client.body.js   #   code.client 函数体
```

---

## 如何导入(给使用者)

### 方式 B:静态挂载(一行命令,推荐 ✅)

```bash
# 安装到 web profile(包自带 dsh.bundle,entry 自动插入插件树)
dsh plugin --profile web add dsh-utility-tools

# 重启 dsh web 生效
```

- 包通过 `dsh.bundle` 声明(`cordis.patch.yml` 内的 `- insert:` 行)自动向插件树插入 entry,**无需手动编辑任何 patch 文件**;
- 安装命令等价于在 profile 目录执行 `pnpm add`;
- 行内 `name` 是模块说明符:loader 经 `import(name)` 加载包入口 `main`(Host ESM),client 模块表扫描 entry 的 `dsh.client` 声明并 serve `./client` 导出(`__ModuleLoader__` 格式);
- Host 半区注册 HTTP 路由 `/dsh-utility-tools/save`,Client 半区经同源 `fetch` 上传附件。

### 方式 A:动态加载(零依赖,已验证)

```bash
git clone <本仓库地址>
```

把 `dynamic/host.body.js` 与 `dynamic/client.body.js` 的全文交给你的 AI:

> 用 cordis_define 创建插件:`code.host` = `dynamic/host.body.js` 全文,`code.client` = `dynamic/client.body.js` 全文,然后 cordis_run 运行。

在 UI 中批准运行请求即可。动态插件是 DSH 一等公民特性,进程内加载、卸载即清理。

---

## 架构说明

| 层面 | 位置 | 职责 |
| --- | --- | --- |
| Client UI | `conversation.input.dock`(id `file-drop-quote`) | 引用条、附件胶囊/管理菜单;document 捕获阶段监听 drop/keydown/click/mouseup 等 |
| Client UI | `conversation.input.overlay`(id `ref-menu`) | 选区上方悬浮「引用」菜单 |
| Client→Host 通道 | 动态:`host.call('save-dropped-file')`;静态:`fetch('/dsh-utility-tools/save')` | 发送时上传附件字节(base64) |
| Host | 动态:`harness.handle`;静态:`webServer.register` 路由 | 纯 JS base64 解码 → `ctx.fs` 写入系统临时目录 `dsh-drops/<会话id>/`(平台自适应 + 重名自动加序号)→ 返回绝对路径 |

### 关键技术点

- **字节无损**:沙箱内置 `atob` 是 UTF-8 感知的(`Buffer.from(b64).toString('utf-8')`),会损坏非 ASCII 字节;Host 端使用纯 JS 手写 base64 解码器,逐字节精确还原;
- **沙箱兼容**:`/tmp` 位于 workspace-write 策略的可写白名单(`writableRoots` = 工作区根 + `/tmp` + `os.tmpdir()`),模型侧工具可直接读写暂存文件;
- **写盘兜底**:`ctx.fs.writeText` 传入 `Uint8Array` 时底层会在原子写盘后抛错(`normalizeLineEndings` 仅支持字符串),以 `fs.stat` 校验大小确认落盘成功;
- **发送拦截**:document 捕获阶段拦截 Enter(textarea 焦点、非 IME 组合、非 Shift)与发送按钮点击(aria-label 匹配「发送消息 / Send message」),先合并引用/附件摘要到草稿再调用 `inputActions.submit()`,同时跳过 `/` 命令流与机器忙态;
- **深色模式**:按钮使用 `--dsw-alias-button-info-fill`(明/暗均为蓝色底白字),避免 `--dsw-alias-brand-primary` 在深色下反转成近白色导致白字不可见。

## 已知限制

- 普通浏览器无法获取拖入文件的真实磁盘路径(平台安全设计),只能按名称引用或暂存到系统临时目录;Electron 环境(`File.path`)可直引原路径;
- 单文件上限 8MB(传输载荷约束),超出仅提示不暂存;
- 暂存目录平台自适应:macOS/Linux 用 `/tmp`,Windows 优先每用户 `%TEMP%`(`C:/Users/<用户>/AppData/Local/Temp`),回退 `C:/Windows/Temp` 等;系统重启后临时目录可能被清空;
- 纯文本引用上限 2000 字符;
- 附件在发送时统一暂存,移除附件不会产生任何磁盘残留;
- 静态版 HTTP 路由仅监听回环地址且只写系统临时目录(受沙箱策略约束),已做最小暴露。

## License

MIT

Install

dsh plugin --profile web add github:sharkymew/dsh-utility-tools

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source