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

文件撤销 QinLuza/dsh-file-undo

DeepSeek Harness
QinLuza56 天前有提交已提供中文文档扫描:含敏感能力

回滚智能体对文件的写入与编辑,支持审查和重做

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

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

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add QinLuza/dsh-file-undo
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:需留意
是什么
dsh 原生插件 · dev
装得上吗
实装验证未通过(dependency_conflict),装前请到仓库确认最近更新与 issue
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
更新放缓:最近一次提交在 56 天前
档位由哪些信号合成

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

验证判据与等级说明 →
⚠︎ 实装验证未通过(dependency_conflict · 2026/10/17) ——可能是验证环境差异,装前建议到 GitHub 仓库确认最近更新与 issue。
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

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

可执行外部命令读写/删除本地文件写入剪贴板(不计风险)发起外部网络请求
查看扫描证据(共 58 处,展示前 53 条)
  • 执行外部命令来自配置/变量
    lib/index.js:498
    const child = execFile(gitBin, args, {
  • 执行外部命令来自配置/变量
    lib/index.js:528
    execFile(candidate, ["--version"], { timeout: 5e3 }, (error) => error ? reject(error) : resolve());
  • 写入本地文件
    lib/index.js:540
    await mkdir(archiveRepoPath(scope), { recursive: true });
  • 发起网络请求
    lib/client.js:352
    const response = await fetch(`/file-undo/api/${method}`, {
  • 写入剪贴板
    lib/client.js:1069
    await navigator.clipboard.writeText(text);
  • 读取本地文件
    lib/store.js:188
    workspaceKeys = await readdir(storeRoot(), { withFileTypes: true }).then((entries) => entries.filter((e) => e.isDirectory()).map((e) => e.name));
  • 读取本地文件
    lib/store.js:196
    chatKeys = await readdir(join(storeRoot(), workspaceKey), { withFileTypes: true }).then((entries) => entries.filter((e) => e.isDirectory()).map((e) =>
  • 读取本地文件
    lib/store.js:203
    const info = await stat(path);
  • 写入本地文件
    lib/store.js:236
    await mkdir(scopeDir(scope), { recursive: true });
  • 读取本地文件
    lib/store.js:278
    text = await readFile(snapshotPath(scope), "utf8");
  • 写入本地文件
    lib/store.js:297
    await writeFile(stagingPath(scope), all.map((s) => `${JSON.stringify(s)}\n`).join(""), "utf8");
  • 写入本地文件
    lib/store.js:298
    await rename(stagingPath(scope), snapshotPath(scope));
  • 写入本地文件
    lib/store.js:303
    await appendFile(snapshotPath(scope), `${JSON.stringify(snapshot)}\n`, "utf8");
  • 读取本地文件
    lib/api.js:588
    hasRecords: existsSync(snapshotPath({
  • 读取本地文件
    smoke-test.mjs:49
    return readFile(target.targetKey, 'utf8')
  • 写入本地文件
    smoke-test.mjs:53
    await writeFile(target.targetKey, content, 'utf8')
  • 写入本地文件
    smoke-test.mjs:132
    await writeFile(fileA, 'hello', 'utf8')
  • 写入本地文件
    smoke-test.mjs:156
    await writeFile(fileA, 'WORLD', 'utf8')
  • 读取本地文件
    smoke-test.mjs:158
    check('write snapshotted before-state', (await readFile(fileA, 'utf8')) === 'WORLD')
  • 写入本地文件
    smoke-test.mjs:162
    await writeFile(fileA, 'WORLD!', 'utf8')
  • 读取本地文件
    smoke-test.mjs:173
    const text = await readFile(scopedSnapshots, 'utf8').catch(() => '')
  • 读取本地文件
    smoke-test.mjs:188
    check('/undo restored edit', undone.kind === 'success' && (await readFile(fileA, 'utf8')) === 'WORLD', undone.text)
  • 读取本地文件
    smoke-test.mjs:193
    check('/undo 0 restored write', undone0.kind === 'success' && (await readFile(fileA, 'utf8')) === 'hello', undone0.text)
  • 写入本地文件
    smoke-test.mjs:197
    await writeFile(join(workspace, 'new.txt'), 'fresh', 'utf8')
  • 读取本地文件
    smoke-test.mjs:202
    try { await readFile(join(workspace, 'new.txt'), 'utf8'); goneCheck = false } catch { goneCheck = true }
  • 执行外部命令固定命令
    smoke-test.mjs:218
    execFileSync('git', ['--version'], { stdio: 'ignore' })
  • 写入本地文件
    smoke-test.mjs:237
    await writeFile(fileA, 'hi', 'utf8')
  • 删除本地文件递归
    smoke-test.mjs:254
    await rm(workspace, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
  • 删除本地文件递归
    smoke-test.mjs:255
    await rm(fakeHome, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
  • 读取本地文件
    src/api.ts:1000
    hasRecords: existsSync(snapshotPath({ workspaceKey: workspaceKeyOf(entry.cwd), chatKey: entry.id })),
  • 发起网络请求
    src/client/api.ts:167
    const response = await fetch(`/file-undo/api/${method}`, {
  • 执行外部命令来自配置/变量
    src/index.ts:613
    const child = execFile(gitBin, args, {
  • 执行外部命令来自配置/变量
    src/index.ts:644
    execFile(candidate, ['--version'], { timeout: 5_000 }, error => (error ? reject(error) : resolve()))
  • 写入本地文件
    src/index.ts:660
    await mkdir(archiveRepoPath(scope), { recursive: true })
  • 读取本地文件
    src/store.ts:281
    workspaceKeys = await readdir(storeRoot(), { withFileTypes: true })
  • 读取本地文件
    src/store.ts:290
    chatKeys = await readdir(join(storeRoot(), workspaceKey), { withFileTypes: true })
  • 读取本地文件
    src/store.ts:298
    const info = await stat(path)
  • 写入本地文件
    src/store.ts:334
    await mkdir(scopeDir(scope), { recursive: true })
  • 读取本地文件
    src/store.ts:424
    text = await readFile(snapshotPath(scope), 'utf8')
  • 写入本地文件
    src/store.ts:447
    await writeFile(stagingPath(scope), all.map(s => `${JSON.stringify(s)}\n`).join(''), 'utf8')
  • 写入本地文件
    src/store.ts:448
    await rename(stagingPath(scope), snapshotPath(scope))
  • 写入本地文件
    src/store.ts:454
    await appendFile(snapshotPath(scope), `${JSON.stringify(snapshot)}\n`, 'utf8')
  • 读取本地文件
    verify-protocol.mjs:32
    const fixtureText = await readFile(FIXTURE, 'utf8')
  • 删除本地文件递归
    verify-protocol.mjs:300
    await rm(tempHome, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
  • 写入本地文件
    verify-prune.mjs:40
    await mkdir(join(tempHome, '.dsh', 'file-undo', scope.workspaceKey, scope.chatKey), { recursive: true })
  • 写入本地文件
    verify-prune.mjs:41
    await writeFile(storePath, seeds.map(s => JSON.stringify(s)).join('\n') + '\n', 'utf8')
  • 读取本地文件
    verify-prune.mjs:78
    const after5 = (await readFile(storePath, 'utf8')).trim().split('\n').map(JSON.parse)
  • 写入本地文件
    verify-prune.mjs:85
    await writeFile(storePath, '', 'utf8')
  • 写入本地文件
    verify-prune.mjs:90
    await writeFile(storePath, JSON.stringify({ filePath: 'x.txt', command: 'edit', before: 'z', time: now - 6 * D }) + '\n', 'utf8')
  • 删除本地文件递归
    verify-prune.mjs:98
    await rm(tempHome, { recursive: true, force: true })
  • 读取本地文件
    verify-rewind.mjs:90
    const text = await readFile(path, 'utf8')
  • 写入本地文件
    verify-rewind.mjs:95
    await writeFile(path, out.join('\n') + '\n')
  • 删除本地文件递归
    verify-visual.mjs:892
    await rm(tempHome, { recursive: true, force: true })
扫描于 2026/11/1(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

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

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

DSH 插件:撤销文件写入/编辑操作(操作前快照,用 /undo 恢复)

README

由 DeepSeek 最新模型翻译生成
dsh-file-undo DSH 插件:撤销智能体对文件的 write / edit 操作。每次文件改动落盘前自动进时间线,侧栏面板红绿 diff 逐条审查,两阶段撤销 + 重做双向循环,多会话自动跟随。当前版本 0.2.0。 可视化面板 侧栏底部「撤销」图标(带未撤销计数徽章)→ 审查面板: - 历史列表:时间、工具徽章(write / edit / create)、文件名、+恢复 / −移除 行数、轮次徽章(该操作发生在模型的第几轮);失败的调用同样留痕 - 撤销视角红绿 diff:绿行 = 撤销将恢复的内容,红行 = 撤销将移除的当前内容,hunk 头 + 双行号,超长截断(800 行后「显示全部」) - 自动预检:打开条目即执行,能否安全撤销逐条给原因(见「安全模型」) - 两阶段确认:第一次点击武装按钮(3 秒自动解除),再点才真正执行 - 会话切换器:列出同一项目树(当前目录及其父子目录)下的全部会话(平台标题 + 活跃/存档状态),选中即钉住;默认自动跟随当前查看的会话 安全模型(两阶段 + 事务 + 乐观并发) | 阶段 | 行为 |
展开完整文档(README 剩余 1 节)
dsh-file-undo

DSH 插件:撤销智能体对文件的 write / edit 操作。每次文件改动落盘前自动进时间线,侧栏面板红绿 diff 逐条审查,两阶段撤销 + 重做双向循环,多会话自动跟随。当前版本 0.2.0。

可视化面板

侧栏底部「撤销」图标(带未撤销计数徽章)→ 审查面板:

- 历史列表:时间、工具徽章(write / edit / create)、文件名、+恢复 / −移除 行数、轮次徽章(该操作发生在模型的第几轮);失败的调用同样留痕
- 撤销视角红绿 diff:绿行 = 撤销将恢复的内容,红行 = 撤销将移除的当前内容,hunk 头 + 双行号,超长截断(800 行后「显示全部」)
- 自动预检:打开条目即执行,能否安全撤销逐条给原因(见「安全模型」)
- 两阶段确认:第一次点击武装按钮(3 秒自动解除),再点才真正执行
- 会话切换器:列出同一项目树(当前目录及其父子目录)下的全部会话(平台标题 + 活跃/存档状态),选中即钉住;默认自动跟随当前查看的会话

安全模型(两阶段 + 事务 + 乐观并发)

| 阶段 | 行为 |
| 预检 preview | 只读分类,逐条给原因:file_creation(创建类撤销 = 删除文件,内容/hash 双闸 + 确认)/ external_modified(快照后文件被手动改过,拒绝吞掉手工编辑)/ superseded_by_later_ops(其后还有 N 条自己的编辑,引导「回退到此状态」)/ already_reverted / file_missing / stale 等 |
| 提交 apply | 携带预检时的 expectedCurrentHash 乐观锁——文件在预览后又变化则拒绝(stale);通过后整文件写回 before 全文,成功标记 reverted |
| 回退 RewindTo | 选中任意历史条目单次原子写回,链条咬合校验 + 明示将丢弃的后续步数,其后条目批量标记 cascade |
| 级联恢复 | 重新应用上游条目时整批复活被连带作废的行(确认框明示「连带恢复 N 条」),撤销/重做双向幂等 |

聊天里的 /undo 不做 external_modified 拦截(保留强制语义)。

会话跟随

面板自动跟随当前查看的会话:client 半订阅 betterSidebar 服务(主源)与 sessions.list(兜底),会话切换后面板所有请求携带该会话标识;host 侧逐请求过平台会话目录核验,cwd 一律从平台读取——伪造的会话 id 一律 404 unknown-session。两源全缺席(headless 等)时静默降级为旧有行为。

命令

| 命令 | 作用 |
| /undo | 撤销最近一次文件写入/编辑 |
| /undo list | 查看当前会话已记录的操作历史 |
| /undo <n> | 撤销指定索引的操作 |
| /undo sessions | 列出同项目树会话(与面板切换器同一条 API) |
| /undo prune [days] | 清理 N 天前的快照(默认 7),只缩回退深度,不动当前文件 |
| /undo git [n] | 从 git 归档恢复第 n 个快照(0 = 最新) |
| /undo git-status | git 归档健康状态 |

原理

1. 全文快照,不存反向补丁:tools/pre-execute 在 write/edit 落盘前捕获 before 全文;tools/post-execute 回填 after 全文 + sha256(外部修改检测的期望哈希)。diff 渲染现场计算,撤销整文件写回,两者解耦。
2. diff 计算与渲染分离:host 侧行级 LCS(前缀/后缀裁剪 + 整数行 ID + Uint32 DP,超大中段降级为整块替换)产出 hunk;客户端按行首字符分类 + 纯 CSS 上色(与主题令牌混色)。
3. 引擎裁决、UI 只展示:能否动文件全部在 host 侧判定(预检分类 + 事务 + 乐观锁),客户端只渲染分类结果。
4. 状态机闭环:recorded → reverted → reapplied,重做后可再撤销。
5. 相对路径按会话解析:工具的 file_path 相对会话工作区,捕获时用会话 cwd 作为解析基准并存稳定标识复用。
6. HTTP API:POST /file-undo/api/*(context / history / detail / preview / apply / reapply / rewind 等),带 Host 头信任围栏(loopback / trustedHosts + sec-fetch-site + Origin 校验)。

快照存储

~/.dsh/file-undo/
<workspace-key>/ # 由会话 cwd 规范化派生(消毒 + 8 位哈希后缀防撞) <chat-key>/ # 会话 id snapshots.jsonl # append-only,单飞写锁 + 原子改名 git-archive/ # 第二层归档:每条快照一个 commit,静默降级

- 同一目录无论 D:\proj / d:/proj/ 哪种写法,恒为同一个键(路径规范化后派生,存量目录零迁移)
- 插件加载时按 7 天惰性清理;/undo prune 手动清理,time 缺失的条目保守保留

安装

方式一:本地构建(link)

git clone https://github.com/QinLuza/dsh-file-undo.git
cd dsh-file-undo && pnpm install && pnpm build

在 profile 的 package.json 里加依赖:

"dsh-file-undo": "link:<本目录绝对路径>"

在 profile 目录执行 pnpm install,重启 DSH 生效。之后的纯 client 半改动只需浏览器硬刷新。

方式二:npm(发布后)

dsh plugin --profile web add dsh-file-undo

限制

- 只覆盖 write / edit 两个工具;在 shell 里直接改文件(如 Set-Content、重定向)不会进入时间线
- 「回退到此状态」会丢弃其后的改动(预检会明示数量),不做旧基线重放
- 面板与自动跟随需要 web 界面;headless 下聊天命令仍可用(会话缝缺席时自动降级)

开发

pnpm typecheck
pnpm build
node verify-prune.mjs # 清理逻辑隔离验证(临时 HOME,不碰真实快照)
node verify-rewind.mjs # 回退语义回归
node smoke-test.mjs # 冒烟测试
node verify-follow.mjs # 会话跟随数据源回归

License

MIT
预览分享卡片长什么样 ↓QinLuza/dsh-file-undo
分享 · DeepSeek Harness Hub
QinLuza/dsh-file-undo

回滚智能体对文件的写入与编辑,支持审查和重做

回到顶部