🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

改前备份 cuteG41cute/dsh-file-history

DeepSeek 客户端
cuteG41cute2 天前有提交已提供中文文档扫描:含敏感能力兼容 / 相关生态

智能体改文件前自动备份,可随时对比恢复

29.9/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/11/2 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

相关生态插件(非 dsh 原生,按其对应运行时安装)
git clone https://github.com/cuteG41cute/dsh-file-history.git

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:仅索引
是什么
生态应用
桌面端 / Web 外壳,不以 dsh plugin add 安装
装得上吗
本站尚未做安装检查
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 2 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

读写/删除本地文件发起外部网络请求
查看扫描证据(共 102 处,展示前 37 条)
  • 写入本地文件
    lib/index.js:220
    await mkdir(dir, { recursive: true })
  • 写入本地文件
    lib/index.js:222
    await writeFile(tmp, data)
  • 写入本地文件
    lib/index.js:224
    await rename(tmp, absPath)
  • 删除本地文件
    lib/index.js:226
    await rm(tmp, { force: true }).catch(() => {})
  • 读取本地文件
    lib/index.js:235
    return await readFile(absPath)
  • 读取本地文件
    lib/index.js:266
    files = await readdir(metaDir)
  • 读取本地文件
    lib/index.js:276
    meta = JSON.parse(await readFile(sidecarPath, 'utf8'))
  • 读取本地文件
    lib/index.js:281
    const content = await stat(meta.backupPath).then(
  • 读取本地文件
    lib/index.js:303
    sigs = await readdir(dir, { withFileTypes: true })
  • 读取本地文件
    lib/index.js:313
    ids = await readdir(sigDir)
  • 删除本地文件
    lib/index.js:422
    await rm(entry.backupPath, { force: true }).catch(() => {})
  • 删除本地文件
    lib/index.js:423
    await rm(path.join(entry.dir, META_DIR_NAME, sidecarName(entry.backupName)), { force: true }).catch(() => {})
  • 删除本地文件
    lib/index.js:455
    await rm(current, { recursive: false, force: true }).catch(() => {})
  • 删除本地文件递归
    lib/index.js:480
    await rm(entryDir, { recursive: true, force: true }).catch(() => {})
  • 写入本地文件
    lib/index.js:523
    await writeFile(
  • 写入本地文件
    lib/index.js:577
    await mkdir(path.dirname(manifestPath), { recursive: true })
  • 写入本地文件
    lib/index.js:578
    await appendFile(manifestPath, `${JSON.stringify(record)}\n`, 'utf8')
  • 删除本地文件
    lib/index.js:1113
    await rm(entry.backupPath, { force: true }).catch(() => {})
  • 发起网络请求
    lib/client.js:19
    return fetch(API, {
  • 读取本地文件
    host-contract-check.mjs:65
    readFile(join(HERE, 'lib', 'index.js'), 'utf8'),
  • 读取本地文件
    selftest.mjs:209
    const files = await readdir(metaDir).catch(() => [])
  • 读取本地文件
    selftest.mjs:214
    const meta = JSON.parse(await readFile(path.join(metaDir, name), 'utf8'))
  • 读取本地文件
    selftest.mjs:226
    workspaces = await readdir(mod.HISTORY_DIR, { withFileTypes: true })
  • 读取本地文件
    selftest.mjs:232
    const sigs = await readdir(path.join(mod.HISTORY_DIR, workspace.name), { withFileTypes: true }).catch(() => [])
  • 读取本地文件
    selftest.mjs:235
    const ids = await readdir(path.join(mod.HISTORY_DIR, workspace.name, sig.name)).catch(() => [])
  • 读取本地文件
    selftest.mjs:239
    const meta = JSON.parse(await readFile(metaPath, 'utf8'))
  • 写入本地文件
    selftest.mjs:257
    await writeFile(target, 'line1\nline2\nline3\n', 'utf8')
  • 写入本地文件
    selftest.mjs:273
    await mkdir(path.dirname(nested), { recursive: true })
  • 写入本地文件
    selftest.mjs:274
    await writeFile(nested, 'export const v = 0\n', 'utf8')
  • 写入本地文件
    selftest.mjs:283
    await writeFile(target, 'line1\nCHANGED\nline3\nline9\n', 'utf8')
  • 写入本地文件
    selftest.mjs:295
    await writeFile(fileA, 'A0\n', 'utf8')
  • 写入本地文件
    selftest.mjs:296
    await writeFile(fileB, 'B0\n', 'utf8')
  • 删除本地文件递归
    selftest.mjs:381
    await rm(gitProject, { recursive: true, force: true }).catch(() => {})
  • 删除本地文件递归
    selftest.mjs:399
    await rm(outerRepo, { recursive: true, force: true }).catch(() => {})
  • 删除本地文件递归
    selftest.mjs:569
    await rm(project2, { recursive: true, force: true }).catch(() => {})
  • 删除本地文件递归
    selftest.mjs:574
    await rm(project, { recursive: true, force: true }).catch(() => {})
  • 删除本地文件递归
    selftest.mjs:575
    await rm(outsideDir, { recursive: true, force: true }).catch(() => {})
扫描于 2026/11/2(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/schemastery
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

DeepSeek Harness 插件:智能体写入或编辑覆盖文件前先备份。按项目生成 .dsh-backup,提供 file_history 恢复工具、顶栏状态面板、按项目开关。

README

由 DeepSeek 最新模型翻译生成
dsh-file-history 一个 DeepSeek Harness 插件,在智能体的 write/edit 覆盖文件之前先备份。 按项目存放 .dsh-backup/(文件名 + 时间戳,保留最近 N 代),提供 file_history 工具,用来列出 / 对比 / 恢复单个文件或整轮改动,顶栏面板显示它是否在运行,还有按项目切换的开关。 备份做不成,就拒绝这次写入(fail-closed)。中文说明见下。 DeepSeek Harness 的「改前自动备份」插件:让智能体改文件这件事从"靠自觉备份"变成"harness 兜底"。 - 任何 write / edit 覆盖已存在文件之前,自动把原文逐字节备份进项目内的 .dsh-backup/; - 备份文件名 = 源文件名 + 时间戳 + 原扩展名,按源文件路径镜像存放; - 每个源文件只保留最近 5 轮(maxGenerations,可就地调整),另加天数与容量两层兜底; - 备份失败就拦下这次写入(fail-closed),不会出现"没备份却照样改坏"; - 模型侧有 file_history 工具(list / show / restore / revert_turn),系统提示里也常驻几行说明, 智能体知道备份系统的存在、位置与恢复方式;
展开完整文档(README 剩余 1 节)
dsh-file-history

一个 DeepSeek Harness 插件,在智能体的 write/edit 覆盖文件之前先备份。 按项目存放 .dsh-backup/(文件名 + 时间戳,保留最近 N 代),提供 file_history 工具,用来列出 / 对比 / 恢复单个文件或整轮改动,顶栏面板显示它是否在运行,还有按项目切换的开关。
备份做不成,就拒绝这次写入(fail-closed)。中文说明见下。

DeepSeek Harness 的「改前自动备份」插件:让智能体改文件这件事从"靠自觉备份"变成"harness 兜底"。

- 任何 write / edit 覆盖已存在文件之前,自动把原文逐字节备份进项目内的 .dsh-backup/;
- 备份文件名 = 源文件名 + 时间戳 + 原扩展名,按源文件路径镜像存放;
- 每个源文件只保留最近 5 轮(maxGenerations,可就地调整),另加天数与容量两层兜底;
- 备份失败就拦下这次写入(fail-closed),不会出现"没备份却照样改坏";
- 模型侧有 file_history 工具(list / show / restore / revert_turn),系统提示里也常驻几行说明, 智能体知道备份系统的存在、位置与恢复方式;
- 运行状况标志:.dsh-backup/_dsh-file-history/status.json(时间戳每次备份/还原刷新)。

兼容性

| dsh | settings 接口 | 状态 |
| 0.1.5-rc.x | settings.register() / .get() | ✅ 老路径 |
| 0.1.7+(含 0.2.x) | 无 register/get:Config + volatile 字段 + settings.mutate(profileEntryId, ops) | ✅ 0.5.0 起 |

插件在运行时用 typeof ctx.settings.register === 'function' 自动分代,同一份代码两代都能跑:

- 0.1.5:注册命名空间,开关写 file-history 命名空间;
- 0.2.x:导出 Config = fileHistorySettingsSchema,读 ctx.fiber.config(volatile 字段 .get() 实时值),写 settings.mutate(<profile 条目 id>, ops)(条目 id 从 configEditor 解析)。
- 客户端面板路由在 0.2.x 上会等 webServer 就绪后再挂载(不把 webServer 放进 inject,否则 headless 配置档会一直 pending)。

0.4.0 在 dsh 0.2.1-rc.5 上会因为 ctx.settings.register is not a function 启动即失败(整条插件不激活)。
升级到 0.5.0 即可;无需改 cordis.patch.yml。

为什么需要它

harness 原生只有 dsh-atomic-write(写入原子)和 dsh-fs-observation-policy(先读后写)—— 原子不等于可撤销。文件被覆盖或删掉之后,唯一的补救是让模型重读上下文再写一遍:既烧 token,又可能二次改错。本插件把原文留在工具层,回滚变成一次工具调用(或一次文件复制)。

备份长什么样

<项目根>/.dsh-backup/ ├── src/lib/nested.20260917-095500.ts # 子目录按源路径镜像 ├── src/lib/nested.20260917-095500.ts.json # 该备份的元数据 ├── demo.20260917-095500.txt ├── demo.20260917-095500.txt.json └── _dsh-file-history/ ├── status.json # ★ 运行状况标志 └── manifest.jsonl # 快照/还原/回退审计流水

~/.dsh/file-history/<工作区>/<文件签名>/<时间戳>/content # 仅“项目外文件”的兜底目录

恢复方式三选一:file_history(action="restore", path=...)、file_history(action="revert_turn")(整轮),
或直接把人能看懂的备份文件复制回原位。

目录

| 文件 | 作用 |
| lib/index.js | 宿主半边:tools/pre-execute 备份 + file_history 工具 + 系统提示公告 + 保留策略 + 客户端 RPC 路由 |
| lib/client.js | 客户端半边:右上角「备份」状态芯片 + 悬停面板(实时流水 / 统计 / 总开关) |
| package.json | 插件清单(dsh.client 段声明浏览器半边,依赖 @deepseek-ai/cordis、@deepseek-ai/schemastery) |
| install.ps1 | 安装/卸载到指定 dsh 配置档(写 node_modules + 登记 cordis.patch.yml,自动备份原文件) |
| selftest.mjs | 离线自测:用假 ctx 驱动已安装副本,70 项断言(宿主加载路径、schema 契约、客户端 RPC、项目级开关、0.1.5/0.2.x 双模式) |
| host-contract-check.mjs | 真依赖契约检查:在 profile 内加载真实 schemastery/timer 语义,12 项断言 |
| monitor.ps1 | 命令行观测面板:一次看清 / -Watch 实时盯梢 / -Scan 扫全机 |
| after-restart-check.ps1 | 重启后体检:插件是否已加载、备份目录是否已创建 |
| verify.ps1 | 端到端验证:起一个独立 headless dsh,让真实智能体改坏再还原 |
| e2e/patch.yml | 端到端验证用的临时补丁 |

可视化观测:右上角状态芯片

装上客户端半边后,会话右上角(「时间戳开 / 记忆库开」那排旁边)会多一个备份芯片:

- 灯色:🟢 最近 2 分钟内有备份 · 🔵 已启用但暂时没动作 · 🔴 有错误(含最近一次失败原因)· ⚪ 读不到状态。
- 芯片文字:当前占用(现存备份的总大小,会随 5 代清理降下来),不是历史累计次数;悬停提示里另给「份数 / 累计次数」。
- 点击展开面板:最后活动时间 / 备份次数 / 保留代数 / 占用 · 统计 / 最近一次备份与还原的完整路径 / 实时流水(每 2 秒刷新,格式 时间 · 动作 · 文件名,鼠标悬停看备份文件路径)/ 一键开关。
- 面板关着时每 30 秒轮询一次(只更新灯色,开销可忽略);开着时每 2 秒。
- 数据来源与命令行面板完全一致:status.json + manifest.jsonl,通过宿主路由 POST /dsh-file-history/api 读取。
- 同一个开关也出现在「设置 → 通用」里(与记忆库的入口一致)。

宿主接口契约(踩过坑,务必按这个写)

这两条是 2026-09-17 两次真实加载失败的根因,已写进 host-contract-check.mjs 做回归:

1. ctx.settings.register(ns, schema, options) 的 schema 必须是 schemastery Schema,不能是工厂函数。 宿主 dsh-settings 的 resolve() 是 schema(mergeLayers(base, section)) —— 把合并后的配置对象
当参数调用 schema。写成 (s) => s.object({...}) 时 s 就是那个普通配置对象,
于是 s.boolean() 不存在 → TypeError: s.boolean is not a function(插件一加载就炸)。
正确写法(与 dsh-memory-db 一致):import s from '@deepseek-ai/schemastery' + s.object({...})。
2. 用到 ctx.setTimeout / ctx.timeout 就必须 inject: ['tools','settings','timer']。 这些方法由 cordis-plugin-timer 通过 ctx.mixin 混入,没 inject 会报 cannot get property "timer" without inject;另外 ctx.setTimeout 已 deprecated,统一用 ctx.timeout。
3. settings 接口分两代(0.1.7 是分界线):0.1.5 有 register()/get();0.1.7+ 没有,改为「导出 Config(schemastery schema,字段标 volatile)+ 读 ctx.fiber.config + 写 settings.mutate(profileEntryId, ops)」。硬写老接口会 TypeError 让整条插件不激活。

安装

方式 A:克隆仓库 + 安装脚本(最直观)

git clone https://github.com/cuteG41cute/dsh-file-history.git
cd dsh-file-history
powershell -ExecutionPolicy Bypass -File install.ps1 -Profile web -Force
然后重启 dsh 服务 / 桌面版窗口

方式 B:pnpm 直接装(不落源码)

dsh plugin --profile web add github:cuteG41cute/dsh-file-history
然后在 profile 的 cordis.patch.yml 里加:
- insert:
- id: dsh-file-history
name: 'dsh-file-history'
inject: [tools, settings, timer, agents]
最后重启 dsh 服务

也可以 npm 方式:npm publish 之后 dsh plugin --profile web add dsh-file-history。

重启是必须的:宿主侧 @deepseek-ai/cordis-plugin-hmr 在 dsh-base 里 disabled: true,改 cordis.patch.yml 不会热加载新插件(客户端插件 HMR 是另一回事);客户端面板还多一步刷新页面。

卸载:powershell -ExecutionPolicy Bypass -File install.ps1 -Remove(已产生的备份不会被删除)。

自测与验证

1) 装依赖(只有跑测试需要)
npm install

2) 离线自测,秒级,70 项断言(假 ctx,含 0.2.x 设置接口)
node selftest.mjs

3) 真依赖契约检查(仓库内或已安装目录都能跑;找不到 schemastery 会回退到 DSH profile 里那份)
node host-contract-check.mjs

4) 重启后的体检 + 端到端(起独立实例,不动正在运行的 3080 服务) powershell -ExecutionPolicy Bypass -File after-restart-check.ps1powershell powershell -ExecutionPolicy Bypass -File verify.ps1

注意:本机 PowerShell 7(pwsh)不在 PATH 上,脚本一律用系统自带的 powershell.exe(Windows PowerShell 5.1)执行。

所有 .ps1 必须带 UTF-8 BOM:Windows PowerShell 5.1 对无 BOM 的脚本按 ANSI(GBK) 解码,脚本里的中文注释/字符串会变成乱码并直接把语法搞坏(Unexpected token)。
用编辑器保存时选「UTF-8 with BOM」,或命令行补一次: powershell $f = 'monitor.ps1'
$text = [System.IO.File]::ReadAllText($f, [System.Text.Encoding]::UTF8)
[System.IO.File]::WriteAllText($f, $text, [System.Text.UTF8Encoding]::new($true))

另外:用 Start-Process 传带空格的路径会被参数拆分吃掉一段,所以 monitor.ps1 支持用环境变量 FH_PROJECT 指定项目目录。

配置(~/.dsh/settings.yaml → file-history 段,改动热生效)

开关是项目级的:enabled 是全局默认,projects 是逐项目覆盖(键=项目目录绝对路径,小写规范化)。
生效值 = 项目覆盖 > 全局默认。右上角面板和「设置 → 通用」里都能直接切当前项目,并能在两种状态间来回。
yaml
file-history:
enabled: true # 全局默认开关(没有单独设置过的项目用它) projects: # 逐项目覆盖:{ "<项目目录>": true | false } c:\work\my-proj: false maxFileBytes: 2097152 # 超过只记指纹(默认 2MB) maxGenerations: 5 # 每个源文件保留最近几代(默认 5 轮) maxAgeDays: 30 # 备份保留天数 maxProjectBytes: 536870912 # 单个项目备份区容量上限(默认 512MB) announceInPrompt: true # 把备份系统写进系统提示 gitignoreBackups: true # 自动把 .dsh-backup/ 加进项目 .gitignore

面板里的三个操作(对应 RPC set-enabled / reset-project):

| 操作 | 效果 |
| 关闭/开启本项目备份 | 写 projects[<当前项目>],只影响这个项目 |
| 改为跟随全局默认 | unset projects[<当前项目>],回到跟随 enabled |
| 全局默认:开/关 → 切换 | 写 enabled,只影响没有单独设置过的项目 |

关键实现决策

1. 不改写工具参数:tools/pre-execute 无法改写 exec.arguments,所以不做"挡下写入再由插件代写"——那会丢掉 harness 原生的 read-before-write 校验与展示层 diff 卡片。这里只做旁路备份 + 校验,原工具照常执行。
2. fail-closed:备份读失败/并发修改 → deny 本次写入,并把原因回给模型。
3. 同轮分组用 rootCallId:一次助手消息里的多次 write/edit 共享 rootCallId,revert_turn 按它成组回退;同一文件多代时回退到本轮最早那代(即本轮开始时的样子)。
4. 备份逐字节保存:不做行尾/编码转换,restore 是原样写回。
5. 代数上限就地执行:每次备份后立刻把该源文件收敛到最近 N 代(不是只靠小时级 sweep),短时间连改也不会堆积。
6. 自身免疫 + git 卫生:.dsh-backup/ 内的文件不再被备份;从工作区向上找最近的 git 根(工作区常是仓库的子目录),把带相对路径的条目追加进那个 .gitignore。
7. 项目外文件不污染无关目录:落到用户主目录的兜底区,由同一个 file_history 工具统一索引。
8. 按宿主真实契约写:见上文「宿主接口契约」——schema 用 s.object(...) 而不是工厂函数,定时器用 ctx.timeout 且声明 inject: ['tools','settings','timer']。离线自测只覆盖调用约定,真依赖语义由 host-contract-check.mjs 在 profile 内保证。

技能与排查

同机已安装配套技能:~/.dsh/skills/dsh-file-history/SKILL.md(含完整行为规则、排障表、file_history 用法)。
预览分享卡片长什么样 ↓cuteG41cute/dsh-file-history
分享 · DeepSeek Harness Hub
cuteG41cute/dsh-file-history

智能体改文件前自动备份,可随时对比恢复

这是你的仓库?认领条目

回到顶部