为 DeepSeek Harness 提供文件增量挂载:自动记录已读文件哪些行进了模型上下文,重复读取只补缺失或改动的部分,并在 Web 端给出可计账的「挂载文件」仪表盘。
- 语言
- TypeScript
- License
- MIT
- 分支
- main
安装
$ dsh plugin --profile web add github:acefun29/dsh-file-mount在终端中运行以上命令,通过 dsh CLI 安装此插件。可在右上角切换 Profile。 第一次用 dsh?看这篇新手教程
对话式安装
帮我安装 DeepSeek Harness 插件 acefun29/dsh-file-mount:先查看仓库 https://github.com/acefun29/dsh-file-mount 确认安全性,然后执行安装命令并验证插件加载成功。
把这段指令粘贴给 DSH Web GUI 里的助手,由它代你完成安装与验证。
一句话定位
dsh-file-mount 把「AI 反复读同一个文件」这种场景下浪费的上下文窗口收回来——它替 DSH 的 read / write / edit 工具记账每个文件哪些行已经进过模型上下文,重复读取只补缺失或磁盘上改动过的行,再附带一个浏览器端的「挂载文件」仪表盘实时展示账本和节省量。
核心能力
- 拦截
read工具结果,对已挂载的行范围用一行「已挂载」标记代替正文,避免整段内容反复塞进上下文 - 当文件磁盘内容发生变化时,按行级 diff 只补改动的行(追加型日志只补新尾巴;中段过大时按唯一行锚点切分 LCS)
- 模型自己刚
write写入的文件被自动标记为「已知道」,下次再读直接免单;edit标记失效但保留行指纹底稿供增量比对 - 提供
file_mount_forget工具,允许模型主动放弃某个文件的挂载、强制下次 read 整本重发 - 在 Web 端注册一个「挂载文件」标签页:列出每个文件、展开成行段、显示覆盖图(已挂载行在全文件中的位置)、按色带标记新鲜度,并展示净节省与粗略人民币折算
- 支持压缩感知:DSH 的标准压缩 checkpoint 出现后,被它 shadow 掉的旧挂载消息不再计入账本,避免基于已被移除的内容做错误去重
技术实现
- 语言: TypeScript(构建产物
lib/index.js+lib/client.js,双面发布) - 关键依赖:
@deepseek-ai/cordis(Service / Context 容器)、@deepseek-ai/schemastery(Config schema)、@deepseek-ai/dsh-tools(defineTool注册file_mount_forget、tools/post-execute拦截钩子)、@deepseek-ai/dsh-llm(createUserMessage注入挂载提示) - 架构模式: 双面 Cordis 插件——
cordis.patch.yml通过 bundle.patch 在宿主挂一行名为file-mount的插件(宿主半部跑tools/post-execute钩子做增量去重),package.json#dsh.client通过dsh-client-ui-conversation的conversation.viewslot 注册file-mount-ui标签页(浏览器半部渲染仪表盘);账本通过结构化source字段挂在标准user/message事件上,宿主压缩 / 会话恢复 / 浏览器折叠共用同一套合并规则(mount-source.ts) - 入口文件:
src/index.ts(宿主半部FileMountService,导出name = 'file-mount'),src/client/index.ts(浏览器半部apply(ctx),导出name = 'file-mount-ui')
适用场景
日常 DSH 跟 AI 协作改一个中等规模项目时,模型经常会反复回头读 package.json、配置文件、工具脚本、UI 组件源码——这些文件里大部分行其实早就进过上下文了,再贴一遍只是浪费窗口。dsh-file-mount 让这种「读同一本书第二遍」变成只贴书签,模型真的需要新内容时才补行;同时也适合让 AI 写完一个文件后再确认自己写了什么——写入动作自动让该文件变成「已知道」,回头读不再付 token。
前置依赖与兼容性
| 依赖 | 最低版本 | 说明 |
|---|---|---|
| DeepSeek Harness | 0.1.0-rc.5(实测) | peerDependencies 全部声明 ^0.1.0-rc.5;plugin 走 cordis.patch.yml 注入宿主、走 dsh.client 注入 Web 客户端 |
| Node.js | ^22.19.0 或 >=24.0.0 | package.json#engines 强制要求;dsh plugin 安装链路还会调用 pnpm |
| 平台 | macOS / Windows / Linux | 无原生模块;paths.ts 仅在路径归一化时按 process.platform === 'win32' 决定是否折叠大小写与盘符 |
| 原生模块 | 无 | 仅依赖 Node 内置 node:fs/promises、node:path,以及上述 npm 依赖 |
安装方式
dsh plugin --profile web add github:acefun29/dsh-file-mount
这是用户在落地页能看到的安装命令。README 提示
github:形式目前只装源码(仓库不含lib/,且没有prepare脚本),实际生产路径更推荐pnpm dsh:install(仓库内置的安装脚本会打 tarball 后用file:E:/...tgz装进 web profile)或使用 GitHub Release 上的预构建dsh-file-mount.tgz。装完必须重启 harness,仅刷新 Web 页面不会激活。
配置项
| 配置 | 类型 | 说明 | 默认值 |
|---|---|---|---|
enabled | boolean | 总开关,关闭后所有 read 原生透传,账本完全停写 | true |
capacity | integer | 文件身份缓存(mtime+size+sha256)的容量上限;已挂载的文件按引用计数钉住不参与淘汰 | 32 |
ttlMs | integer | 缓存安全阀:mtime+size 未变但内容实际已改时的兜底重读间隔(毫秒) | 300000(5 分钟) |
maxPinnedFiles | integer | 单个会话最多钉住多少个挂载文件(LRU 超过即淘汰最久未用) | 256 |
minSavedTokens | integer | 净节省低于这个 token 数时放弃本次去重/增量,原生透传且不写账本,也不计入安全阀次数 | 16 |
maxFingerprintBytes | integer | 超过这个字节数的文件不留行级底稿,发生改动时只能整本重挂 | 1000000 |
maxManagedBytes | integer | 超过这个字节数的文件根本不接管,原样放行 | 16777216(16 MiB) |
excludeGlobs | string[] | 这些路径永远原样放行,不写账本也不入仪表盘 | [](典型写法:['**/node_modules/**']) |
statsFile | string | 可选:跨会话总账的 JSON 落盘路径,宿主可通过 ctx.fileMount.stats() 读取 | 未设置 |
freshnessEnabled | boolean | 是否启用「新鲜度」启发式:上下文接近窗口上限时把靠前的段判过期、下次读重发 | true |
freshnessThreshold | number | 段位次低于此阈值(0..1)即判过期 | 0.6 |
safeRatio | number | 当前上下文 / 窗口 低于此比例时,压力视为 0,不判过期 | 0.95 |
safeTokens | number | 可选:绝对安全 token 上限;设置后覆盖 safeRatio | 未设置 |
pinAfter | integer | 段被标记为过期的次数达到此值后钉住,最多只重发这一次 | 1 |
contextWindow | integer | 当会话未报告窗口大小时使用的默认上下文窗口 token 数 | 128000 |
resendBudget | number | 可选:大于此 token 估计的段即使过期也不摘账本,避免大段重新塞进上下文 | 未设置 |
valveReads | integer | 连续 N 次完全覆盖去重后,下一次 read 放行原生结果并刷新相关段(防呆;0=关闭) | 2 |
配置写在
cordis.patch.yml同级的config节点下;schema 校验和默认值见src/index.ts:103-121。
常见问题
Q: 这个插件和 DSH 自带的会话压缩、上下文截断是什么关系?
A: 完全不冲突,也不互相替代。会话压缩解决「当前窗口还装得下多少原始历史」,dsh-file-mount 解决「同一段文件内容反复塞进窗口」——前者看上下文,后者看文件账本,两者方向正交,可以同时启用。压缩发生后,被它 shadow 的旧挂载消息会自动从账本剔除,下次读取重新锚定。
Q: 为什么读文件时 Web 端 UI 的 read 卡片变成了通用卡片,原本的文件高亮/代码块没了?
A: 这是预期行为。插件在 tools/post-execute 替换了模型可见的结果文本(去重 marker 或增量正文),UI 的 read 卡片是按结果文本渲染的,所以降级到通用版本;canonical value 原样保留,下游审计日志不受影响。
Q: 卸载或禁用插件之后我的挂载账本还在吗?
A: 内存账本随会话结束而释放,磁盘上不存任何账本文件(除非主动配置了 statsFile)。下次重新启用时账本会从当前会话的注入消息 source 字段重新回放——所以关闭后短时间内重新打开,数据是连续的;关闭前若想保留跨会话累计统计,需要先把 statsFile 配出来。
Q: 仪表盘里某个文件的「行范围」列表太细,看不过来怎么办?
A: 点击文件行左侧的折叠箭头可以收起该文件的全部行段;顶栏提供搜索(按路径模糊匹配)和排序(按净节省或按路径名)。色带只能区分档位,不能调阈值——所有 freshness 阈值(freshnessThreshold / safeRatio / safeTokens / pinAfter)只在宿主配置里调整,仪表盘不可改。
Q: 增量补行的时候,万一行级 diff 漏改了怎么办?
A: 不会发生静默漏改。插件用 stat 校验(mtime+size 快路径 + sha256)确认磁盘身份,文件只要内容变了就走 hash 不一致分支;行级底稿丢失或改动过大时自动回退到「整本重挂」而不是放弃该文件。所以漏改只可能发生在 ① 文件超过 maxFingerprintBytes 没留底稿、② mtime+size 未变但内容实际改了的极端情况下,后者由 ttlMs 安全阀兜底。
Q: 文件名大小写、软链接、相对路径会不会识别错?
A: 账本内部用「绝对路径 + realpath + 大小写折叠(按文件系统实测,Windows / 默认 macOS 折叠、Linux 不折叠)」做唯一键,不会被软链接、相对路径绕开;模型看到的纸条头则用工作目录的相对路径(正斜杠),工作目录优先取会话 header.cwd,没有则取 dsh-fs-local 的 cwd。
上手难度
进阶 — 装上即用的默认配置就能工作,普通用户无需理解账本、哈希、行级 diff 也能受益;但要让仪表盘指标符合预期(比如 valveReads、pinAfter、safeRatio 这些阈值),需要先理解 DSH 的「工具结果 → 模型上下文」链条和 Cordis 配置机制。
已知问题与限制
- 压缩(compaction)后「已挂载」保证失效:被 checkpoint shadow 的挂载内容已离开模型上下文,插件通过
sourceEventSeqs识别并剔除,下次读取重新锚定;UI 端的 shadow 列表没通道同步,旧行会保留到该文件下次重挂 - 增量 / 去重 / 重挂都替换了工具结果文本,Web 端 read 卡片因此降级为通用卡片(canonical value 完整保留,审计不受影响)
- 依赖 read / write / edit 工具的 canonical value 结构;若 DSH 改动这些形状,插件守卫直接退化为原生透传,并在首次触发时向会话注入一次警告(
src/index.ts:872-885)。形状变更由集成测试钉死 - 超过
maxManagedBytes(默认 16 MiB)的文件与excludeGlobs命中的路径不接管,原样放行;插件不做抽样指纹——抽样有「改了没看出来」的风险 - rc.6 的自定义会话事件类型无法安全持久化(持久化读取路径会硬性拒绝未知类型),所以账本载体选用标准
user/message事件上的结构化source字段,而不是新增自定义事件类型 - 「新鲜度」是启发式:段过期不代表内容真的被移出上下文(只有压缩才会),而是「注意力已衰减、模型基本看不见」,所以过期重发是刻意的 token 开销;没有 usage 数据的会话(如某些适配器)显示灰色「未知」,不判过期
- 浏览器端的会话视图是分页历史窗口(默认尾页 50 条消息、上滚才加载更早页),仪表盘折叠会跨快照累积,挂载消息滚出窗口后该文件行仍保留在列表里,直到下次该文件重挂
- 仪表盘的「点行跳回聊天」、跨会话总账的界面展示、「文件已变更」实时角标暂未实现(浏览器端没有对应通道),需要看细节请去查配置或日志
- 安装提示:Windows 上
dsh plugin add .会把盘符拼进 profile 路径导致插件装上但不激活,必须走pnpm dsh:install或预构建 tarball;npx @deepseek-ai/dsh第一次跑可能长时间无输出(它在拉完整 CLI 包)
DeepSeek Harness 插件:文件增量挂载 + 重复读取去重。记录每个文件哪些行范围已经进入模型上下文,重复读取只补缺失部分;文件在磁盘上变化时按行级对比只补改动的行;并提供「挂载文件」仪表盘实时展示账本。
移植自 piwpi 的 context-mount 机制。
效果
- 模型侧:读过的行范围不重复进上下文(去重 marker);缺失/改动的正文写进本次 read 的工具结果(增量 / 重挂),纸条只记账本声明;文件改动后只补改动的行(行级 diff,日志追加只补新尾巴);AI 自己写过的文件回头读直接免单;
file_mount_forget工具让模型能主动强制重读。 - 界面侧:「挂载文件」标签页是仪表盘——打开时停在顶部,净节省与路径搜索固定在顶栏,文件列表单独滚动;每个文件行可展开成文件段列表,每段带新鲜度色带(绿=新鲜/黄=一般/橙=接近过期/红=已过期/灰=未知)和过期次数;另有覆盖图(色块标出已挂载行在文件中的位置)、搜索、排序、净节省与人民币折算;对话区有上下文注入折叠行,「文件已变更」时行上有角标。
- 节省统计:中文按 1 字 ≈ 1 token、其他按 4 字符 ≈ 1 token 估算;同时记账「省下的」和「纸条花掉的」,界面显示净值(为负时按 0 显示);可选把跨会话总账落盘(
statsFile)。
安装
一个包两面:dsh.bundle.patch 挂载宿主插件行,dsh.client manifest 让 Web 端扫描出浏览器半部。装进 profile 后必须重启 harness(刷新页面不够)。需要本机有 pnpm(dsh plugin 转调它)和 Node ^22.19 || >=24。
1. GitHub Release(推荐)
npx --yes @deepseek-ai/dsh plugin --profile web add https://github.com/acefun29/dsh-file-mount/releases/latest/download/dsh-file-mount.tgz
npx --yes @deepseek-ai/dsh --profile web
已有全局 dsh 时把第一行换成 dsh plugin --profile web add https://github.com/acefun29/dsh-file-mount/releases/latest/download/dsh-file-mount.tgz。装的是预构建包,无需 allowBuilds,也不走 npm。
若 npx @deepseek-ai/dsh 长时间没输出,多半在拉 CLI;等它结束,或先用本机已经跑过的 DSH(~/.dsh/profiles/node_modules/@deepseek-ai/dsh)。
2. 本仓库开发版
pnpm dsh:install
安装器会打包 tarball,并用 file:E:/...tgz 交给 pnpm。Windows 上不要对目录路径用 dsh plugin add . 或 file:E:\...(pnpm 会把盘符拼进 profile 目录,插件装上但不激活)。
3. 本地 tarball
pnpm run build
npm pack --ignore-scripts
dsh plugin --profile web add file:$(pwd)/dsh-file-mount-$(node -p "require('./package.json').version").tgz
Windows PowerShell:
pnpm run build
npm pack --ignore-scripts
$Tgz = ((Get-Location).Path -replace '\\','/') + "/dsh-file-mount-$((Get-Content package.json -Raw | ConvertFrom-Json).version).tgz"
npx --yes @deepseek-ai/dsh plugin --profile web add "file:$Tgz"
不要用 github:acefun29/dsh-file-mount 装源码:仓库不含 lib/,且包已去掉 prepare。请用上面的 Release / 安装器。
然后启动:npx @deepseek-ai/dsh --profile web。
配置
- id: file-mount
name: dsh-file-mount
config:
enabled: true # 总开关;关闭后所有读取原生透传
capacity: 32 # 文件身份缓存容量(挂载中文件不受淘汰影响)
ttlMs: 300000 # 缓存安全阀:同 stat 内容被改的兜底重读间隔
maxPinnedFiles: 256 # 单个会话最多钉住多少个挂载文件
minSavedTokens: 16 # 去重/增量净收益低于此值则原生透传且不写账本(也不计入安全阀次数)
maxFingerprintBytes: 1000000 # 超过此大小的文件不留行级底稿(改动时整本重挂)
maxManagedBytes: 16777216 # 超过此大小的文件不接管,原样放行
excludeGlobs: ['**/node_modules/**'] # 这些路径永远原样放行
statsFile: ./dsh-file-mount-stats.json # 可选:跨会话总账落盘路径
freshnessEnabled: true # 新鲜度:默认开
pinAfter: 1 # 过期一次后钉住,该段最多只被重发一次
contextWindow: 128000 # 会话未报告窗口时的默认 W
# resendBudget: 8000 # 可选:大于此 token 的段本轮不摘除
valveReads: 2 # 重读安全阀:连续拦截达到此次数触发原生透传重读(0=关闭)
## 工作原理
插件挂在 `tools/post-execute` 拦截面,按工具名分流:
1. **read**:以 canonical value(path/offset/lines/totalLines)为准确定本次窗口;经 stat 校验式缓存(mtime+size 快路径 + sha256)核实磁盘身份后三分支决策:完全覆盖 → 结果换成去重 marker(同一个文件在两次真实消息之间只发第一条去重纸条,重复去重静默合并节省);部分覆盖 / hash 变化 → **缺失或改动的正文写进本次 read 的工具结果**(每行带 `N: ` 行号,与原生 read 对齐;`cancel` 清 inbox 最多丢掉账本纸条,下次当没挂过再发),纸条只留 head-only 账本声明;hash 变化时拿行级底稿做 diff,**只补改动的行**(没动的行号平移;中段过大时用唯一行锚点切分 LCS),没底稿或改动过大则整本重挂。首次挂载仍保留原生 read 正文 + head-only 纸条。
2. **write**:AI 写完整文件,整本书标记为「已知道」,回头读直接免单;缓存指纹同时作废。
3. **edit**:标记缓存失效但保留行指纹底稿,下一次读必重读盘并走行级 diff,只补改动行。
4. 挂载状态结构化写入注入消息的 source(标准 `user/message` 事件),恢复重放与浏览器折叠共用同一载体、同一套合并规则(`mount-source.ts`)。
5. 压缩感知:识别 DSH 标准压缩 checkpoint(source `{kind:'plugin', plugin:'compact'}` 的 `sourceEventSeqs`),被 shadow 的挂载消息不再计入账本。
6. 模型可调用 `file_mount_forget` 工具主动作废某个文件的账(强制重读)。去重 marker 会提示:上文找不到内容时,先 forget 再 read。
7. **新鲜度**:挂载段记录载体消息的 `seq`,按它在当前上下文中的位置判断是否还适合去重。接近窗口上限时,越靠前的内容越容易被摘账,下次读取会重发;过期一次后钉住。压缩才会真正把内容移出上下文。另有重读安全阀(连续全覆盖去重达到次数后放行原生 read)。新鲜度不提供界面调节。
路径身份:账本用绝对路径 + `realpath`(软链接统一到真实文件)+ 大小写折叠(按文件系统实测,Windows/Mac 默认折叠)。模型可见的纸条 head 用相对工作目录的路径(正斜杠),工作目录取自会话 `header.cwd`,没有则用 `dsh-fs-local` 的 `cwd`。
## 已知限制
- compaction 后「已挂载」保证失效:被压缩掉的挂载内容离开模型上下文,插件靠 checkpoint 的 `sourceEventSeqs` 识别并跳过,下一次读取重新锚定。
- 增量 / 去重 / 重挂载替换了结果文本,UI 的 read 卡片降级为通用卡片(canonical value 完整保留)。
- 依赖 read / write / edit 工具 canonical value 的结构;结构变化时守卫失效并原生透传(集成测试锁定)。
- 超过 `maxManagedBytes` 的文件与 `excludeGlobs` 命中的路径不接管,原样放行(不做抽检:抽检有「改了没看出来」的风险)。
- 自定义会话事件类型在 rc.6 无法安全持久化,故账本载体选用标准事件上的结构化 source。
- 新鲜度是启发式:段过期不代表内容被移出上下文(只有压缩才会),而是「注意力已衰减、模型基本看不见」,故过期重发是故意的 token 开销;无 usage 数据的会话(如某些适配器)显示灰色「未知」,不判过期。
- 浏览器会话是分页历史窗口(默认尾页 50 条消息,上滚聊天才加载更早),仪表盘折叠跨快照累积,挂载消息滚出窗口后文件行仍保留;被压缩 shadow 的旧挂载在宿主侧已摘账,但浏览器端看不到 shadow 清单,行会保留到下一次该文件重挂。
- 仪表盘「点行跳回聊天」、跨会话总账的界面展示、「文件已变」实时提示暂缓(浏览器端没有对应通道)。
## 常见问题
- **为什么读文件时 UI 的 read 卡片变成通用卡片?** 插件在 post-execute 替换了模型可见的结果文本(去重 marker / 增量或重挂正文);canonical value 原样保留,但卡片按结果文本渲染,所以降级为通用卡片。
- **怎么让插件少管一些文件?** `excludeGlobs` 配排除名单(如 `**/node_modules/**`),`maxManagedBytes` 配大文件上限;名单外/超大文件原样放行。
- **省的数字准吗?** 是估算:中文 1 字 ≈ 1 token,其他 4 字符 ≈ 1 token;界面显示净值(省下的 − 纸条花掉的,为负时显示 0),并按每百万 token ≈ ¥1 粗略折算人民币。
- **模型想强制重读一个文件?** 调 `file_mount_forget` 工具作废该文件的账,下次读整本重发。去重结果也会写明:上文找不到就先 forget 再 read。
- **跨会话统计怎么看?** 配置 `statsFile` 后自动累计到该文件,可通过 `fileMount.stats()` 读取;界面展示暂缓。
- **`npx @deepseek-ai/dsh plugin add …` 一直没输出?** npx 在拉完整 CLI 包,可能要好几分钟。用户安装请用 GitHub Release 的 `dsh-file-mount.tgz` 地址;本仓库开发用 `pnpm dsh:install`。Windows 上不要 `add .`,用 `file:E:/...tgz`(正斜杠)。
- **装上了但没有「挂载文件」标签?** 目录安装在 Windows 上会链到错误路径,插件不会进 `dsh.profile.bundles`。改用 Release 包或 `pnpm dsh:install` 后重启 harness。
## 开发
```sh
pnpm install
pnpm test # vitest(191 用例:单元 + 真实 read/write 循环集成 + 持久化往返 + 压缩感知 + 新鲜度 + 客户端组件 + 安装契约)
pnpm typecheck # tsc --noEmit
pnpm run build # tsc + tsdown(lib/index.js / lib/client.js)
pnpm dsh:install # 打 tarball 并装进本机 web profile(Windows 可用)
打 GitHub Release:打 v* 标签并 push,CI 会上传稳定文件名 dsh-file-mount.tgz(releases/latest/download/dsh-file-mount.tgz)。暂不发布 npm。
升级 DSH 后先跑一遍测试:压缩 checkpoint 的标记形状等耦合点由测试钉死(tests/compaction.spec.ts),DSH 改形状时测试会立刻报警。
依赖 DSH 0.1.0-rc.5 及以上(peer 依赖 @deepseek-ai/dsh-*、@deepseek-ai/cordis ^4、React 18)。
License
MIT
查看使用指南 →
该插件的安装步骤、关键要点、FAQ 与兼容性说明(基于已收录字段派生)。
收录徽章
[](https://deepseek-plugin.org/plugins/acefun29/dsh-file-mount)把这段 markdown 粘贴到你的 GitHub README,链接回本插件详情页。徽章只声明已被本站收录,不代表安全认证。