# dsh-plugin-attachment-library · 附件库 [English](README.md) | 中文 DeepSeek Harness(DSH)插件:在 **设置 → 附件库** 中查看本机保存的所有图片和文件附件、它们占用的空间,以及哪些对话仍在使用它们(正常、已归档、等待移入回收站、在回收站)。 **只读。** 目前不提供删除:插件不会创建、修改、移动或删除任何附件、会话记录、回收站文件或缓存;“在访达中显示”只选中文件,不打开、不执行。插件唯一的写入是“用预览打开”时在系统临时目录(DSH 数据之外)放置的只读副本,插件停用时删除。它的用途是先核对引用统计,为以后是否安全清理附件提供依据。 - 兼容 DSH 0.2.x(`engines.dsh: >=0.2.0-rc.2 <0.3.0`,含 0.2.1-alpha 预览版) - 会话回收站的统计需要配合 [`dsh-plugin-session-trash`](https://github.com/ZZZZZZZ-ZZZZZ/dsh-session-trash);没有安装它时,只统计现存对话 ## 安装 **桌面版**:打开侧栏的 **插件** → **添加插件**,在“包名或地址”中填入以下任一项,点 **安装**,完成后点 **立即启用**(新装的插件默认关闭): - GitHub 地址:`https://github.com/ZZZZZZZ-ZZZZZ/dsh-attachment-library`,指定版本可写 `github:ZZZZZZZ-ZZZZZ/dsh-attachment-library#v0.4.1` - npm 包名:`dsh-plugin-attachment-library` - 本地安装包的绝对路径,例如 `/path/to/dsh-plugin-attachment-library-0.4.1.tgz`(由 `pnpm pack` 生成) 不要填写源码目录:源码目录的 `node_modules` 里有开发用的 DSH 包副本,会和 DSH 自身的包冲突。 **命令行**(可选):完全退出 DSH 后运行 `dsh plugin --profile desktop add dsh-plugin-attachment-library`。 **卸载**:在 **插件** 页面卸载“附件库”。插件不保存任何数据,卸载不影响附件。 ## 页面内容 - **统计**:附件总数与总占用;其中图片、文件各多少;被现存对话使用、只被回收站里的对话使用、无任何对话引用各多少(个数和占用);被引用但文件已缺失的数量;可再生的缩放缓存占用。 - **列表**:类型图标(图片为图片图标,文件按扩展名显示)、类型、文件名(同一内容有多个文件名时全部保留)、大小、图片尺寸与格式、存入时间,以及引用它的每个对话的标题和状态。同一附件被多个对话共用时全部列出。 - **用预览打开**(仅 macOS):已保存图片行的箭头图标按钮(悬停显示“用预览打开”),在主机上用“预览”打开这张图片。打开的是带正确扩展名的只读副本,不是 DSH 的原文件(原因见下文)。文件行没有这个按钮。 - **在访达中显示**:每个已保存附件行右侧的文件夹图标按钮(悬停显示“在访达中显示”),在主机的访达(Windows 资源管理器、Linux 默认文件管理器)中选中该文件:文件附件显示为它的真实文件名,图片显示为按内容命名的对象文件。只有页面连接的是本机 DSH、主机有桌面文件管理器且 DSH 允许桌面打开时才显示;失败原因显示在该行下方。 - **重新统计**:右上角的刷新图标按钮(悬停显示“重新统计”);统计进行中按钮不可用,并显示“正在重新统计…”。 - **筛选**:类型(全部 / 仅图片 / 仅文件)和引用情况(全部 / 现存对话 / 仅被回收站引用 / 无人引用);**排序**:按时间或按大小(都是从新到旧、从大到小)。列表每次显示 100 条,可继续展开。 - **读取不完整的提示**:如果有会话记录读不全(损坏、被截断),页面会列出它们,并提示“无人引用”等统计可能偏多。 ## 图片为什么在访达里没有预览 DSH 按内容保存图片:文件名就是图片的 SHA-256 摘要(例如 `attachments/v1/objects/3f/3f9a…`),**没有扩展名**,权限为只读(0400)。内容是普通的 JPEG、WebP、PNG 或 GIF(经过归一化),没有加密;只是 macOS 靠扩展名判断文件类型,所以访达不显示缩略图,双击也不会当作图片打开。文件附件不同:它们另有一个带真实文件名的只读硬链接,在访达里正常显示。 ## 统计规则 | 规则 | 说明 | |---|---| | 附件身份 | DSH 按内容(SHA-256)去重保存附件;图片与文件分别保存,同样的字节作为图片和作为文件是两条记录 | | 引用来源 | 扫描每条会话记录中的所有 `{ type: 'image' \| 'file', attachment: { attachmentId } }`:用户消息、工具结果、系统消息都包括 | | 归属到对话 | 子智能体会话的引用算在发起它的对话上;fork 出的会话是独立对话(它继承了父对话的前缀,所以两者都会列出) | | 现存对话 | 由会话存储列出的全部会话(通过官方只读句柄读取),状态为正常、已归档或等待移入回收站 | | 回收站 | 直接读取回收站目录中的会话日志(原始 JSONL 或 Zstandard 压缩),标题取移入回收站时记录的标题 | | 使用中 / 仅回收站 / 无人引用 | 有任意一个回收站外的对话引用即为“使用中”(等待移入回收站的也算);只有回收站里的对话引用为“仅回收站”;都没有为“无人引用” | | 占用 | 图片对象和文件对象各计一次;文件名链接是硬链接,不额外占空间;缺失的附件不计入占用 | | 存入时间 | 附件对象的修改时间(第一次保存该内容的时间) | ## 缓存 第一次打开页面会读取全部会话记录,记录很多时可能较慢。之后: - 现存会话按会话存储给出的 revision 判断是否变化,只重新读取有变化的会话; - 回收站会话按日志文件的设备号、inode、大小和修改时间判断; - 图片尺寸按对象文件的同一组信息缓存。 缓存只保存在 DSH 进程内存中,不写入磁盘;重启 DSH 后的第一次统计会重新读取全部记录。 ## 配置 DSH 的所有目录都只读不写。 | 字段 | 默认值 | 说明 | |---|---|---| | `attachmentDirectory` | `/attachments/v1` | 附件存储目录;如果给 `dsh-attachment-local` 配置了 `dshHome`,这里要改成对应路径 | | `trashDirectory` | `/session-trash` | 会话回收站目录;应与 `dsh-plugin-session-trash` 的 `trashDirectory` 一致 | | `requestCacheDirectory` | `/cache/attachments/request-images` | 按模型缩放的图片缓存(可再生),只统计大小 | | `previewDirectory` | 系统临时目录 | 在其中创建存放“用预览打开”副本的私有临时目录;这是插件唯一会写入的位置 | ## “在访达中显示”如何工作 - 使用 DSH 自己的打开能力:主机端调用官方 `@deepseek-ai/dsh-native-command` 的 `revealNativePath`(macOS 为 `open -R`,Windows 为资源管理器 `/select`,Linux 打开所在目录),与 DSH 内置“在访达中显示”使用同一个函数;可用性按同一套检查(`nativeFileManager()`、`canOpenNativePath()`)判断,浏览器端还要求 `ctx.remote.$host.isLoopback` 为真,并遵从 DSH 官方的 `session.canOpenWorkspacePath()` 开关。 - 没有直接复用官方的 `session.openWorkspacePath`:它要求浏览器传入绝对路径,并且只接受已映射到工作区文件系统的路径,附件目录不在其中。 - 浏览器只发送附件类型和 id(`sha256:` 加 64 位小写十六进制),经过认证的 `/api` 网关严格校验,任何多余字段(包括路径)都会被拒绝。 - 路径由主机根据 id 推出:文件为 `attachments/v1/files/<前缀>/<摘要>/<文件名>`(取按名称排序的第一个文件名;没有时用 `file-objects` 中的对象),图片为 `attachments/v1/objects/<前缀>/`。目标必须是附件目录内的普通文件,路径上不能有符号链接;否则返回“找不到”。 ## “用预览打开”如何工作 - 浏览器只发送图片的 id(`sha256:` 加 64 位小写十六进制),网关严格校验,多余字段(包括路径)一律拒绝。主机按 id 找到 `attachments/v1/objects/<前缀>/`,要求它是附件目录内的普通文件、路径上没有符号链接;文件附件、缺失的图片和链接都返回“找不到”。 - 主机读取图片头确定格式,把图片复制到私有临时目录(`/dsh-attachment-preview-<随机>/`,权限 0700)中,副本名为摘要前 16 位加扩展名(`.jpg`、`.webp`、`.png`、`.gif`),权限 0400;复制前核对内容与摘要一致,同一张图片重复打开时复用副本。 - 打开副本而不是原文件,是因为原文件没有扩展名,而且虽然是只读(0400),文件所有者仍可在“预览”里“解锁”修改它,系统打开文件时也可能给它附加元数据;副本让 DSH 的原文件完全不被触碰。 - 用 DSH 官方的 `@deepseek-ai/dsh-native-command`:先用 `nativeFileApplications` 确认“预览”已登记为该副本的处理程序,再用 `openNativeFileApplication` 打开,它执行固定参数的 `open -a /System/Applications/Preview.app <副本路径>`(`execFile`,不经过 shell)。 - 只在 macOS 主机(访达可用)且页面连接本机 DSH、DSH 允许桌面打开时显示;插件停用时删除整个私有临时目录。 ## 只涉及本机数据 附件库只列出本机上保存的内容;删除本机副本(例如用会话回收站永久删除对话)不会删除 DSH 已经发送给 DeepSeek 的内容: - 使用 DeepSeek 官方 API 时,DSH 默认随每次请求上传会话记录(官方插件 `@deepseek-ai/dsh-session-log-deepseek`)。在设置中关闭“在使用官方模型 API 时上传 Session Log”只会停止之后的上传;重新打开时,关闭期间记录的内容也会被上传。 - 发送给官方 API 的图片会作为文件上传,DSH 请求 DeepSeek 在 7 天后使其过期,但不保证远端一定删除。 本插件不发出任何网络请求,不向任何地方发送数据;页面通过 DSH 带认证的 `/api` 只收到附件名、大小和对话标题,不会收到文件路径。 ## 已知限制 - 不统计、也无法管理已上传到 DeepSeek 服务端的图片(索引在 `/llm-deepseek/files-v3.json`)。 - 只认识 DSH 0.2.x 的附件存储布局(v1)和 `dsh-plugin-session-trash` 的回收站格式(manifest 版本 1);之后的大版本需要重新验证。 - 不校验附件内容的 SHA-256,只按文件存在与大小统计;被引用但文件缺失的附件会单独标出。 - 会话存储无法读取的会话(例如更新版本 DSH 写入、或已损坏)不计入引用,会出现在“读取不完整”的提示中。 - 一次统计结果包含完整列表;附件数以万计时,返回数据较大、首次统计较慢。 - 在访达中打开附件所在文件夹时,访达自身可能在该文件夹写入 `.DS_Store`(界面状态文件);这是访达的行为,插件本身不写任何文件。 - Linux 上只能打开所在目录,不能选中文件。通过远程访问打开 DSH 页面时不显示该按钮。 - “用预览打开”只支持 macOS;副本保留到插件停用(或 DSH 退出后由系统清理临时目录),DSH 异常退出时可能留在系统临时目录中。 - 只在 macOS 上测试过;插件对 DSH 数据只读,预计在 Linux 和 Windows 上同样可用。 ## 相关插件 **[会话回收站 · dsh-plugin-session-trash](https://github.com/ZZZZZZZ-ZZZZZ/dsh-session-trash)**:在侧栏会话菜单里加入“移到回收站”,会话可以恢复,或再次确认后永久删除;本次启动中打开过的会话会先隐藏、下次启动时再移入。两个插件同时安装时,附件库会显示哪些附件只被回收站里的对话使用。 安装:在 **插件 → 添加插件** 中填 `github:ZZZZZZZ-ZZZZZ/dsh-session-trash#v0.2.1`。 ## 开发 ```sh pnpm install pnpm run typecheck pnpm test # 构建 lib/ 后运行全部测试 ``` `lib/` 随仓库提交,供从 GitHub 安装时直接使用;修改源码后请运行 `pnpm run build` 并一起提交 `lib/`。 测试使用 npm 上发布的 DSH 0.2.0-rc.2 包:真实 JSONL 会话存储、真实工作区注册表与 Typert 网关。附件按 `dsh-attachment-local` 的目录布局生成(含硬链接文件名),回收站条目按 `dsh-plugin-session-trash` 的格式生成(含 Zstandard 多帧压缩、被截断的帧和不完整的行),全部在临时目录中;每个场景都会对比附件、会话和回收站目录在统计、“在访达中显示”和“用预览打开”前后逐字节、逐 inode 不变。测试中把官方 `revealNativePath` 替换为记录调用;“用预览打开”运行真实的官方 `nativeFileApplications` / `openNativeFileApplication`,只把它们的命令执行器换成记录器,并断言传给它的固定参数,不会真的打开访达或预览。客户端测试从 `tests/vendor/ui-primitives` 加载真实的 0.2.0-rc.2 界面组件(DSH 源码原样副本,MIT 许可证,见 `tests/primitives-shim.ts`);不需要 DSH 源码目录。