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

哈希锚点编辑 Rianico/dsh-better-edit

DeepSeek Harnessv1.2.0
Rianico5 天前有提交已提供中文文档扫描:含敏感能力

一个专为 DeepSeek Harness 而生的更好的编辑工具。

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

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

自动兼容检查未通过:engines.node 要求 >=24,不满足 Node 22.19.0。以下命令仍可尝试,但可能失败。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add Rianico/dsh-better-edit
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:需留意
是什么
dsh 原生插件 · platform
装得上吗
静态安装检查未通过,可能需要源码安装
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 5 天前
档位由哪些信号合成

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

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查需源码安装

engines.node 要求 >=24,不满足 Node 22.19.0

展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包dsh-better-edit @ 1.2.0
✗Node 引擎要求 >=24 · 基线 Node 22.19 不满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

源码安装时若 pnpm 拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑。

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/30 09:05:00

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

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

读写/删除本地文件
查看扫描证据(共 62 处,展示前 60 条)
  • 读取本地文件
    benchmark/run.mjs:59
    const CORPUS = readFileSync(corpusPath, "utf8");
  • 读取本地文件
    src/canonical-path.ts:43
    const st = lstatSync(candidate);
  • 读取本地文件
    src/canonical-path.ts:74
    const candidateStats = await lstatAsync(candidatePath);
  • 读取本地文件
    src/canonical-path.ts:80
    await readlinkAsync(candidatePath),
  • 读取本地文件
    src/file-view.ts:218
    const pathStat = await fsStat(filePath);
  • 读取本地文件
    src/file-view.ts:311
    await fsAccess(absolutePath, accessMode);
  • 读取本地文件
    src/file-view.ts:387
    const stats: FileStats = preloadedStats ?? (await fsStat(canonicalPath));
  • 读取本地文件
    src/fs-bridge.ts:402
    const bytes = await readFile(absolutePath);
  • 读取本地文件
    src/fs-bridge.ts:421
    const bytes = await readFile(absolutePath);
  • 读取本地文件
    src/fs-bridge.ts:437
    return readFile(absolutePath, "utf-8");
  • 读取本地文件
    src/fs-write.ts:16
    const entries = await readdir(dir, { withFileTypes: true });
  • 读取本地文件
    src/fs-write.ts:22
    const stats = await stat(tempPath);
  • 删除本地文件
    src/fs-write.ts:24
    await rm(tempPath, { force: true });
  • 读取本地文件
    src/fs-write.ts:54
    existingStats = await stat(targetPath);
  • 写入本地文件
    src/fs-write.ts:62
    await writeFile(targetPath, content, "utf-8");
  • 写入本地文件
    src/fs-write.ts:69
    await mkdir(dir, { recursive: true });
  • 删除本地文件
    src/fs-write.ts:80
    await rm(tempPath, { force: true });
  • 写入本地文件
    src/fs-write.ts:88
    await rename(tempPath, targetPath);
  • 写入本地文件
    src/fs-write.ts:93
    await writeFile(targetPath, content, "utf-8");
  • 删除本地文件
    src/fs-write.ts:97
    await rm(tempPath, { force: true });
  • 删除本地文件
    src/fs-write.ts:104
    await rm(tempPath, { force: true });
  • 读取本地文件
    src/guidance/materialize.ts:109
    content = await readFile(path, "utf-8");
  • 写入本地文件
    src/guidance/materialize.ts:116
    await writeFile(path, seededContent(section), { encoding: "utf-8" });
  • 写入本地文件
    src/guidance/materialize.ts:134
    await mkdir(homeDir, { recursive: true });
  • 写入本地文件
    src/guidance/materialize.ts:138
    await mkdir(dir, { recursive: true });
  • 读取本地文件
    src/guidance/materialize.ts:139
    const existing = new Set(await readdir(dir));
  • 写入本地文件
    src/guidance/materialize.ts:147
    await writeFile(path, seededContent(section), {
  • 删除本地文件
    src/guidance/materialize.ts:165
    await unlink(ghost).catch((error: unknown) => {
  • 读取本地文件
    src/guidance/materialize.ts:174
    const entries = await readdir(homeDir, { withFileTypes: true });
  • 读取本地文件
    src/guidance/materialize.ts:180
    const existing = new Set(await readdir(dir));
  • 删除本地文件
    src/guidance/materialize.ts:193
    await unlink(ghost).catch((error: unknown) => {
  • 读取本地文件
    src/guidance/materialize.ts:199
    const homeFiles = new Set(await readdir(homeDir));
  • 写入本地文件
    src/guidance/materialize.ts:207
    await writeFile(join(homeDir, file), content, {
  • 读取本地文件
    src/guidance/resolve.ts:124
    const content = await readFile(candidate, "utf-8").catch((error: unknown) => {
  • 写入本地文件
    src/hash-store.ts:1158
    await rename(candidate, `${candidate}${suffix}`);
  • 读取本地文件
    src/hash-store.ts:1187
    await stat(row.path);
  • 写入本地文件
    src/hash-store.ts:1205
    await mkdir(dirname(storePath), { recursive: true });
  • 读取本地文件
    src/hash-store.ts:1206
    let existed = existsSync(storePath);
  • 读取本地文件
    src/snapshot-store/migrate.ts:31
    content = await readFile(legacyPath, "utf-8");
  • 写入本地文件
    src/snapshot-store/migrate.ts:81
    await rename(legacyPath, `${legacyPath}.bak`);
  • 写入本地文件
    src/store-config.ts:163
    await mkdir(dir, { recursive: true });
  • 写入本地文件
    src/store-config.ts:164
    await writeFile(yamlPath, DEFAULT_CONFIG_YAML, { encoding: "utf-8", flag: "wx" });
  • 读取本地文件
    src/store-config.ts:179
    const raw = readFileSync(yamlPath, "utf-8");
  • 读取本地文件
    src/store-config.ts:393
    const raw = readFileSync(yamlPath, "utf-8");
  • 写入本地文件
    src/store-config.ts:403
    appendFileSync(yamlPath, suffix, "utf-8");
  • 读取本地文件
    src/store-config.ts:413
    mtime = statSync(yamlPath).mtimeMs;
  • 读取本地文件
    src/store-config.ts:440
    mtime = statSync(yamlPath).mtimeMs;
  • 读取本地文件
    src/store-lifecycle.ts:47
    const content = readFileSync(join(ws, ".gitignore"), "utf-8");
  • 读取本地文件
    src/store-lifecycle.ts:64
    if (!existsSync(join(ws, ".git")) || gitignoreHasEntry(ws)) return;
  • 读取本地文件
    src/store-lifecycle.ts:69
    if (!existsSync(gitignorePath)) {
  • 写入本地文件
    src/store-lifecycle.ts:70
    appendFileSync(gitignorePath, ".dsh_better_edit/\n");
  • 读取本地文件
    src/store-lifecycle.ts:72
    const content = readFileSync(gitignorePath, "utf-8");
  • 写入本地文件
    src/store-lifecycle.ts:74
    appendFileSync(gitignorePath, `${prefix}.dsh_better_edit/\n`);
  • 读取本地文件
    src/store-lifecycle.ts:117
    entries = await readdir(runtimeDir);
  • 读取本地文件
    src/store-lifecycle.ts:147
    const st = await stat(dir);
  • 删除本地文件递归
    src/store-lifecycle.ts:209
    await rm(info.dir, { recursive: true, force: true });
  • 读取本地文件
    src/store-tenancy.ts:42
    if (existsSync(wsPathFile)) {
  • 读取本地文件
    src/store-tenancy.ts:43
    const existing = readFileSync(wsPathFile, "utf-8").trim();
  • 写入本地文件
    src/store-tenancy.ts:51
    mkdirSync(dir, { recursive: true });
  • 写入本地文件
    src/store-tenancy.ts:52
    writeFileSync(wsPathFile, `${canonicalWs}\n`, "utf-8");
扫描于 2026/11/3(0 天前) · 范围 全包扫描(包内未声明入口) · 来源 GitHub 仓库

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

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

功能亮点

  • 哈希锚点文件编辑
  • 提供读写与撤销工具
  • 降低token消耗

为 DeepSeek Harness(dsh)提供基于哈希锚点的 read/edit/undo_last_edit 工具,token 消耗更少,成本更低。

README

由 DeepSeek 最新模型翻译生成
dsh-better-edit 一个专为 DeepSeek Harness 而生的更好的编辑工具。 底层基于 hash-anchored 定位——不依赖行号,也不靠字符串替换,更少的词源消耗,留出更多的上下文空间给真正的工作。 English · 快速开始 • 为什么用 Hashline • 基准测试 • 工具 • 致谢 _"瓶颈在于 harness——而不是模型。"_ —— Can Bölük,[_The Harness Problem_] 这是 harness 的修复。 内容哈希取代行号——上方编辑永不使下方锚点移位。每个范围都对照 Agent 实际看到的内容校验。过期或未见的行被硬拒绝并回传新锚点重试——无需 read。 3 次调用 vs 6 次 · -55.8% tokens · 23/23 正确性。 同样的外部漂移重构,同样正确的文件(单次随机运行 vs OMP;完整方法)。 大多数编辑工具要求模型在改动任何东西之前,先逐 token 复述旧代码——而这正是 Agent 最容易出错的地方:多个模型在 replace 式编辑下的补丁格式失败率高达 46–51%。dsh-better-edit 走得更远。文件的每一行都分配一个唯一的 3 字符内容哈希,编辑时按哈希定位。
展开完整文档(README 剩余 1 节)
dsh-better-edit

一个专为 DeepSeek Harness 而生的更好的编辑工具。
底层基于 hash-anchored 定位——不依赖行号,也不靠字符串替换,更少的词源消耗,留出更多的上下文空间给真正的工作。

English ·

快速开始 • 为什么用 Hashline • 基准测试 • 工具 • 致谢

_"瓶颈在于 harness——而不是模型。"_ —— Can Bölük,[_The Harness Problem_]

这是 harness 的修复。 内容哈希取代行号——上方编辑永不使下方锚点移位。每个范围都对照 Agent 实际看到的内容校验。过期或未见的行被硬拒绝并回传新锚点重试——无需 read。

3 次调用 vs 6 次 · -55.8% tokens · 23/23 正确性。 同样的外部漂移重构,同样正确的文件(单次随机运行 vs OMP;完整方法)。

大多数编辑工具要求模型在改动任何东西之前,先逐 token 复述旧代码——而这正是 Agent 最容易出错的地方:多个模型在 replace 式编辑下的补丁格式失败率高达 46–51%。dsh-better-edit 走得更远。文件的每一行都分配一个唯一的 3 字符内容哈希,编辑时按哈希定位。旧文本从不回显,锚点在编辑后依然有效,每个解析出的范围都会与模型实际看到的内容逐一核对——错行编辑不可能悄悄落盘。

为什么需要它

str_replace 会让模型逐字复述它要替换的代码——纯粹的转录成本(输出 token,按约 5-6 倍输入计费),也是 Agent 最容易出错的地方:真实模型补丁失败率高达 46–51%,块越大越糟,每次失败都要重新读取并重试。

Hashline 用两个哈希代替旧文本——编辑 token 减少 31%(多行范围达 43%)——并对照模型所见内容校验每个范围:编辑要么落在你想要的行的位置,要么响亮失败并回传新锚点。锚点是内容地址,上方编辑后依然有效,连续编辑无需重读;上下文更精简,模型的注意力也保持在代码上,而不是复述上。

[!TIP]
亮点——诚实且可衡量:

- 自愈而非静默。 外部编辑永不被覆盖——过期范围被拒绝并以全新 HASH│content 重发重试;孤儿 served 条目无需全量重读即可自愈(ADR-0008)。Fail-closed,而非自动合并。
- 格式化容忍。 ASCII 空白不敏感锚点在 prettier/black/eslint --fix 之间存活(formatOnSave、监听、CI)。仅限 linter 场景——字符串内的空白不区分(ADR-0005)。
- 链式与批量,无重读仪式。 未受影响行的锚点保持有效;diff/回显/拒绝行即视为已提供。edit 最多 32 个同文件编辑原子执行([E_BATCH_ABORT]),相比 str_replace 信封约 -40%。
- 读守卫。 从未展示过的行永不被编辑——[E_RANGE_UNSERVED]/[E_RANGE_UNVERIFIED]/[E_STALE_ANCHOR] 在写入前拒绝,随后 reject-and-serve。
- 更少往返。 实测:同样外部漂移重构,OMP 封装需 6 次调用,hashline 仅 3 次;信封 vs str_replace 的节省是稳定值—— upstream 电池可复现(算法相同)。

不适用于单行小改动(接近持平)或新建文件(用 write)。它的价值在长会话与结构性编辑中体现——任何不允许改错行的场景。

快速开始

安装

npx @deepseek-ai/dsh plugin --profile web add github:Rianico/dsh-better-edit # 从 github
npx @deepseek-ai/dsh plugin --profile web add dsh-better-edit # 从 npm
npx @deepseek-ai/dsh plugin --profile web add /path/to/dsh-better-edit # 从本地源码

该 profile 的下一个会话将带着 hashline 工具运行。验证该层是否生效:

dsh --profile <name> --dump-config # 会显示 "# == dsh-better-edit" 层

| 要求 | |
| Node | ^22.19.0 \|\| >=24.0.0(dsh 的要求;存储使用 node:sqlite) |
| Profile | 一个 dsh profile(首次使用 dsh plugin 时初始化) |
| 后端 | 支持沙箱/远程文件系统(写入经 ctx.fs) |
| dsh | 0.2.1-rc.2(已端到端验证;0.1.6-alpha.2 是上一个 0.2 之前的基线) |

支持 dsh 0.2.0 及以上。 注册走 agent/created,路径解析跟随会话的实时工作目录(workingDirectory,0.2.1 之前回退到 session header),工具指引的 order 取自 systemPrompt.getSectionOrder。没有 peer 版本范围,也不做运行时版本探测:兼容性来自这层稳定接口加可选服务回退——缺少 workingDirectory 或 agentPresets 的组合同样可用。

read 返回的每一行都带有哈希前缀——哈希就是这一行的地址:

ve7│function hello() {
szJ│ console.log("world");
kQm│}

edit 按哈希范围定位,因此编辑总能落在你想要的行的位置:

{
"path": "src/main.ts",
"edits": [["szJ", "szJ", " console.log('hi');"]]
}

并产生带全新锚点的 diff,让下一次编辑无需重新读取即可通过校验:

− szJ │ console.log("world");
+ a3m │ console.log('hi');
kQm │ }

详见快速开始中配置一节的指引覆盖与存储租约。

配置

租约与提示词指引都只需声明一次,在 agent/created 时读取,无需改代码。

存储租约 — 默认 central

默认无仓库污染。存储位于 $DSH_HOME/plugins/dsh-better-edit/runtime/<name>-<hash8>/(central)。

[!NOTE]
DB 文件是可丢弃的缓存——下次调用 read 时会自动重建(哈希值由文件内容重新推导)。可随时 rm -rf 任意 runtime/<name>-<hash8>/ 或 hash-store.sqlite 以回收磁盘;不会丢失用户数据(仅丢失锚点历史和撤销快照)。

如需回退到旧的同址或自定义根:

$DSH_HOME/plugins/dsh-better-edit/config.yaml
storeDir: central # 存储位置:"central"(默认,位于 $DSH_HOME/.../runtime/<name>-<hash8>/)| "workspace"(旧版 <ws>/.dsh_better_edit/)| "/abs/path"(自定义根目录) autoGitignore: false # 仅 workspace:当 .git 存在但 .gitignore 缺少 .dsh_better_edit/ 时,true = 自动追加 ".dsh_better_edit/",false = 仅告警一次 undo_ttl_s: 604800 # 撤销历史 TTL(秒);-1 = 永久保留(默认 604800 = 7 天) storeMaxAgeS: 2592000 # central 清理:runtime/<name>-<hash8>/ 目录最大闲置时长(秒),超时后被清理(默认 2592000 = 30 天) storeMaxTotalBytes: 524288000 # central 清理:runtime/ 下总字节数上限,超限后按 LRU 清理(默认 524288000 = 500 MB)

[!TIP]
若文件不存在,首次启动时会自动生成带注释的默认 config.yaml(永不覆盖已有文件)。

Env 覆盖 yaml(env > yaml > central),非法回退到 central 并告警:

DSH_BETTER_EDIT_STORE_DIR=central|workspace|/abs
DSH_BETTER_EDIT_AUTO_GITIGNORE=true|false # 大小写不敏感

旧的 <workspace>/.dsh_better_edit/ 在首次以 central 打开时一次性拷贝;runtime/<name>-<hash8>/ 可通过 ls 查看,带 .wsPath 旁路文件。工作区身份跟随会话的实时工作目录(working_directory 工具),而不是会话创建时的目录。生命周期详见 docs/specs/issue-24-store-tenancy.md(janitor mtime>storeMaxAgeS 后 LRU、WAL checkpoint、undo TTL)。

按 preset 的指引

tool:read / tool:edit / tool:undo_last_edit 的提示词片段是按 agent preset 覆盖的纯 Markdown 文件,无需改插件:

$DSH_HOME/plugins/dsh-better-edit/<preset>/.md

(默认主目录 ~/.dsh,即 ~/.dsh/plugins/dsh-better-edit/)。片段对照表:

| 文件 | 提示词片段 | 默认 order |
| read.md | tool:read | 1100 |
| edit.md | tool:edit | 1300 |
| undo_last_edit.md | tool:undo_last_edit | 1310 |

首次启动时插件会为四个随附 preset——standard/、ptc/、minimal/、cordis/——各自写入编译内置的可编辑指引文件(含 order front-matter)。默认 order 取自 harness 自己的工具槽位(systemPrompt.getSectionOrder),上游调整 band 时会自动跟随;front-matter 可自行钉死。详见配置详情的重置/恢复。完整规范:docs/specs/issue-24-store-tenancy.md 与指引文档。

重置 / 恢复默认

想让指引“回到默认”?删掉覆盖文件,或把它清空(同时删掉开头的 --- 栅栏)即可。

清空文件时,只要文件里是空白内容、并且没有 --- 栅栏,插件就认为你不想再自定义这一片段了,于是:

- 新会话开始时,直接使用插件内置的默认指引;
- 下一次启动插件时,会用默认内容把这个文件重新生成。

再细致一点:

- 为什么要连栅栏一起删:只要文件里还有 --- 栅栏,哪怕是空的、什么内容也没有,插件都当成“你有意留空”,不会去动它。所以“故意留空”就保留栅栏;“恢复默认”就把栅栏和内容一起清掉。
- 栅栏写错了会怎样:少了一个 ---、order 不是数字、出现了不认识的键——只要栅栏格式不对,插件会直接忽略这个文件:仍然渲染默认指引,并在日志里提示是哪个文件、哪里写错了。你写的正文会原样留在磁盘上,方便改好后再用。
- 自带 preset 与自定义 preset:插件自带的四个 preset(standard、ptc、minimal、cordis)会在下一次启动时自动补全空白文件;自定义 preset 里删除的覆盖文件不会自动出现(删了就当“没有这个文件”)。删掉某一整个 preset 目录,下次启动时插件会重新生成里面的三个片段文件(仅限自带 preset)。
- 恢复的是“当前版本”的默认:插件升级后,重置得到的是新版本带的新默认内容,不是旧版本留下的。

文件的重新生成只发生在插件启动时,绝不影响进行中的会话。

为什么用 Hashline

省 token。 一次编辑调用只携带 remove_from / remove_to(两个 3 字符哈希)加替换文本——从不回显被替换的文本。str_replace 调用则必须逐字复现被替换的文本。在一个真实文件上的 12 次编辑会话中,这可以减少 31% 的输出 token(多行范围达 43%)——而且这些是输出* token,按输入的约 5-6 倍计费。见基准测试。

但这从来不是“最省 token”。 节省随被替换文本的规模增长——单行微调时几乎持平——而且像 @oh-my-pi/hashline 这样的紧凑补丁语言还能发出更轻的负载(同一会话中 42–53%)。关键在于正确形态的编辑调用:不复述旧代码,模型除了两个稳定的内容地址外无需跟踪任何东西。

正确性。 每个解析出的编辑范围都会与模型实际看到的行逐行核对。过期、从未提供或歧义的范围会在写入任何内容之前被硬性拒绝,并把当前范围以全新锚点的形式回显(reject-and-serve)——重试无需 read。

面向 Agent 的现代编辑范式。 内容地址锚点与行号无关:编辑文件的一部分,其余行的哈希保持不变,因此连续编辑无需重新读取。模型按行是什么来定位,而不是按它之前在第几行。

对比

| | hashline edit | str_replace(Claude Code / Codex) | @oh-my-pi/hashline 补丁 |
| 调用中永不回显被替换文本 | ✅ 只有两个哈希 | ❌ 逐字回显 | ✅ 只有 + 行 |
| 按什么定位行 | 内容哈希 | 文本匹配 | 行号 + 文件内容标签 |
| 对照模型所见内容校验 | ✅ 每一行 | ❌ 取第一个匹配 | ~ 仅文件版本 |
| 检测文件已过期 | ✅ 拒绝并回传新锚点 | ❌ 可能匹配到错误位置 | ✅ 标签不匹配 → 拒绝或三方合并 |
| 上方编辑后锚点依然有效 | ✅ 内容寻址 | ✅ 基于内容 | ❌ 重新编号 + 新标签 |
| 连续编辑无需重读 | ✅ diff 提供新锚点 | ~ | ~ 从编辑响应取行号 |
| 文本重复时无歧义 | ✅ 边界锚点需校验 | ❌ 取第一个出现 | ~ 按位置,行未逐行校验 |
| 错行编辑永远不会悄悄落盘 | ✅ 每一行都校验 | ❌ 取第一个匹配 | ~ 原则上可能(标签只校验版本,不校验行) |
| 块操作 / 寄存器 / MV / REM | ❌ | ❌ | ✅ |
| 一次变更一个文档 | ❌ 每次一个调用 | ❌ 每次一个调用 | ✅ 多 hunk 补丁 |
| 运行时 | ✅ Node(dsh) | — | ⚠️ 仅 Bun |
| 撤销 | ✅ 持久化 | ❌ | ❌ 不在范围内 |

~ = 偶尔/不稳定。@oh-my-pi/hashline 是一种紧凑的行锚定补丁语言(npm、仓库):[path#tag] 头把每个 hunk 绑定到全文件内容哈希,PUT N.=M: 按行号定位;每次编辑都会重新编号——下一次的行号与标签取自编辑响应或重新 read。

不同的工作,同一条血脉。 两者都源于 harness-problem 的洞见:模型绝不该复述旧代码。@oh-my-pi/hashline 是补丁语言库——负载更轻(每次编辑省 42%,单个批量文档省 53%,见基准测试),支持语法块操作(PUT N:)、寄存器、REM/MV、多 hunk 文档、可插拔文件系统(任何后端),以及标签过期时的会话感知三方合并恢复。本插件则是一对 dsh 工具:read 把 3 字符内容哈希交给模型,edit 取其中两个,并对解析出的每一行对照已提供状态校验——无需重新编号、无需重新取标签、错误锚点永远不可能落到错误的行,undo_last_edit 重启后依然有效。代价:每次编辑的 JSON 外壳会多一点负载、没有块操作,并且它活在 dsh(Node)内部,而不是独立补丁器(Bun)。要跨后端的补丁格式选 hashline 库;要在 Agent 里做可校验、内容寻址的编辑,选 hashline 工具。

边界情况下的正确性

token 基准测试衡量的是模型发出的负载——它假设模型每次都能拿到正确的地址,而且免费。正确性才是两种 hashline 实现真正分道扬镳的地方。下面是 harness-problem 文献里的真实故障模式(错行编辑、漂移、重复文本),以及各自在遭遇它们时的表现:

| 边界情况 | hashline edit(本插件) | @oh-my-pi/hashline 补丁 |
| 错误地址(锚点/行号差一行) | 不可能——锚点解析到具体行;解析出的每一行都对照已提供状态校验,在写入任何内容之前被拒绝 | 可能——当前标签下的错误行号会悄悄落到错误位置;标签只证明文件版本,从不证明行 |
| 模型查看后文件在磁盘上被改动 | 硬拒绝 + 回传新锚点(reject-and-serve);重试无需 read | 标签不匹配 → 拒绝或对未知的当前内容做尽力而为的三方合并 |
| 上方编辑导致文件移位 | 什么都不移位——锚点是内容地址;diff 提供新锚点 | 每次编辑都重新编号——“RE-GROUND AFTER EVERY EDIT” 是它自己的头号规则;账由模型记 |
| 重复/相同文本 | 每行哈希唯一(冲突已消解);歧义 → [E_AMBIGUOUS_ANCHOR] | 基于位置,重复不会混淆——但位置本身未被校验 |
| 从未展示给模型的行 | [E_RANGE_UNSERVED]——硬拒绝并回传新锚点 | 未展示的 hunk 被拒绝——同样依赖模型知道自己看过什么 |
| 表达式中间 / 错误的块节点 | 无关——任何已校验的行范围都合法 | 语法规则 + PUT N: 节点选择;点错(锚在 def 会让装饰器变成孤儿)会悄悄落错;无语法检查 |
| 多编辑批量中途失败 | edit 多条目——原子、全有或全无;失败项以新锚点回显 | 多段补丁先预检——同样原子 |

oh-my-pi 42–53% 的负载节省来自更轻的线格式;上表才是该格式反过来要求模型记在脑中的东西——重新编号、追标签、选节点——而这恰恰是最容易出错的组件(替换式编辑的补丁失败率 46–51%)。本插件 31% 的代价买来的是一个“错编辑落不了地、任何拒绝都不需要重读”的契约。

基准测试

在同一份 103 行文件上、用相同的 12 组替换(8 个单行、4 个 3/6/10/15 行多行),以固定的 js-tiktoken cl100k_base 词表测量。三个被测方发出相同的替换文本:本插件的 edit(两个 3 字符锚点)、str_replace 工具(逐字回显旧文本)、以及 @oh-my-pi/hashline 的两种模式——每次编辑一个 [path#tag] 段(seq)和一个多 hunk 批量文档(batch):

| 指标 | hashline | str_replace | oh-my-pi seq / batch |
| 被替换文本是否上线 | ✅ 从不 | ❌ 每次编辑都发 | ✅ 从不 |
| 输出 token 节省(12 次编辑) | ✅ 31% | ❌ 0% | ✅ 42% / 53% |
| 多行范围节省(3–15 行) | ✅ 29–47% | ❌ 0% | ✅ 40–53% |
| 按 5 倍输出计价的实际成本 | ✅ 低约 1.4 倍 | ❌ 1× | ✅ 低约 1.7 倍 / 2.1 倍 |
| 范围对照已提供状态校验 | ✅ 100% | ❌ 无 | ~ 仅文件版本 |
| 模型需要跟踪的行号 | ✅ 无——内容锚点 | ✅ 无——文本匹配 | ❌ 每次编辑重新编号 |
| 确定性、可在本地复现 | ✅ npm run benchmark | — | — |

可复现

上面的数字是确定性的,你可以本地复现——npm run benchmark:

| 场景 | 行数 | hashline | str_replace | oh-my-pi seq | oh-my-pi batch |
| 单行 ×8 | 1 | 309 | 324 | 241 | — |
| 多行 ×4 | 3–15 | 393 | 691 | 349 | — |
| 合计 ×12 | | 702 | 1015 | 590 | 480 |

相对 str_replace 的节省:hashline 313(31%) · oh-my-pi 逐次 425(42%) · oh-my-pi 批量 535(53%)。

脚本天然确定:固定语料、内容寻址且自带自检的编辑脚本(语料被重排会直接抛错,而不是悄悄改变测量对象)、固定版本的 tokenizer,且 oh-my-pi 负载在计数前会对照其发布的语法校验。因为一切都是固定的,npm run benchmark 对每个人都是同一个结果——本 README 里的数字就是该次运行的一个快照;重新生成,不要轻信。

范围与诚实。 基准测试衡量的是请求负载 token——每次编辑调用时模型发出的内容——读文件流量完全相同故已排除(可抵消),替换文本也完全一致。它没有建模转录失败与重试,而真实差距恰恰主要在那边:最初的 harness-problem 文章报告改用锚定编辑后输出 token 减少 61%,补丁失败率从 46–51% 降至接近零。它同样没有建模行号格式在调用之间让模型付出的代价——每次编辑后重新编号、重新获取文件标签——也不包括块操作能力、Bun 与 Node 的运行时差异,以及 @oh-my-pi/hashline 是独立补丁器、而本插件是带 read/edit/undo 的 dsh 工具对这一事实。完整方法论、逐编辑表与完整局限清单见 benchmark/README.md。这些数字背后的正确性差距见上文边界情况下的正确性。

工具

| 工具 | 作用 |
| read | 以 HASH│内容 形式返回文件。参数:offset(1 起始)、limit。分页输出以 [Showing lines N-M of T. Use offset=… to continue.] 结尾。超过 200KB 的行显示为标记并附 sed 提示——哈希锚点需要完整行。 |
| edit | 对象根负载 { "path": path, "edits": [[remove_from, remove_to, replacement_text], …] };path 可为 null 以通过锚点推断。单个条目编辑一个范围;多条目对同一文件原子批量(最多 32)。对每个包含范围内的每一行校验,reject-and-serve 返回新锚点。 |
| undo_last_edit | { path } 撤销该文件上一次 hashline 编辑,仅当文件仍与存储的编辑后内容一致时生效;重启后依然有效。 |

内置 write 仍用于整文件替换。执行前,插件只会在候选行以同一会话、规范路径及行位置所提供的精确 HASH│ 锚点开头时拒绝;文件保持不变,并提示仅用文件正文重试。插件不会泛化地剥离类似哈希的文本。

错误码

| 代码 | 含义 |
| [E_ACCESS] | 文件存在但工具不可读/不可写。 |
| [E_AMBIGUOUS_ANCHOR] | 一个哈希匹配当前多行;调用 read 获取新锚点。 |
| [E_BAD_OP] | 范围结束先于范围开始(首尾颠倒时会自动纠正)。 |
| [E_BAD_REF] | remove_from/remove_to 不是裸 3 字符哈希。 |
| [E_BAD_SHAPE] | 请求/字段形态错误(未知字段、缺少 path、非字符串文本等)。 |
| [E_BARE_HASH_PREFIX] | HASH│ 前缀被粘贴进 replacement_text(自动纠正)。 |
| [E_BATCH_ABORT] | 批次内某项失败;整个批次被拒绝,未写入任何内容。 |
| [E_FILE_TOO_LARGE] | 文件超过 hashline 行数上限;请改用 write 或其他方式。 |
| [E_INVALID_PATCH] | diff 预览标记被粘贴进 replacement_text(自动纠正)。 |
| [E_NOOP_LOOP] | 完全相同的编辑反复不产生任何变化;再次提交会被拒绝。 |
| [E_NOT_FOUND] | 文件不存在。 |
| [E_NOT_OBSERVED] | 该文件在本会话中尚未被观察(先读后写策略);请先调用 read。 |
| [E_NOT_TEXT] | 路径是目录、二进制或非 UTF-8 文件;hashline 只能编辑文本。 |
| [E_WRITE_HASH_ECHO] | 内置 write 候选内容复制了同一会话、路径及行位置所提供的 HASH│ 预览锚点。写入在执行前被拒绝;移除整条复制的锚点链后重试。 |
| [E_RANGE_STALE] | 某行自被读取以来在磁盘上发生变化;范围以全新锚点回显。 |
| [E_RANGE_UNSERVED] | 范围内包含从未提供给模型的行。 |
| [E_RANGE_UNVERIFIED] | 边界锚点无法对照已提供状态验证。 |
| [E_STALE_ANCHOR] | 锚点不再能解析;调用 read 获取新锚点。 |
| [E_UNDO_STALE] | 无法撤销:编辑之后文件被修改(或删除)。 |
| [E_UNDO_UNAVAILABLE] | 撤销历史无法持久化;编辑未被应用。 |
| [E_WOULD_EMPTY] | 编辑会把非空文件清空;请用 write 清空。 |

如何替换内置工具

dsh 的工具注册表按作用域解析:agent 看到的是 agent → preset → global,且自身层总是优先。内置的 read/edit 位于 agent-preset 层,因此普通的全局注册无法替换它们。本插件:

1. 通过其 cordis.patch.yml bundle 补丁作为宿主层 Cordis 插件挂载。
2. 在 agent/created 时,将 hashline 工具以及 tool:read / tool:edit 提示词片段注册到 agent 自身的作用域层——从而为该 agent 遮蔽 preset 的内置工具,并在 agent 销毁时自动解除。
3. 保留内置的 write,通过作用域内的 tools/pre-execute 监听器在执行前拒绝精确的同会话/同路径/同行锚点回显,并由 tools/post-execute 在成功结果后附加 hashline 自动读取。

存储

哈希快照、已提供状态行与撤销历史存放在一个 SQLite 库中——默认 central($DSH_HOME/plugins/dsh-better-edit/runtime/<name>-<hash8>/hash-store.sqlite,可通过 ls 查看,带 .wsPath 旁路文件)。旧的同址 <workspace>/.dsh_better_edit/ 仍可通过 config.yaml 中 storeDir: workspace 启用,并在首次以 central 打开时一次性拷贝。不同工作区的并行会话各自持有独立的库(会话 cwd 会随每次工具调用传递),因此一个项目的锚点与撤销历史不会泄漏到另一个项目。在工具调用之外(测试、预览)会回退到共享的 DeepSeek Harness 主目录($DSH_HOME/plugins/dsh-better-edit/hash-store.sqlite)。

7 天 TTL 清理已提供的行;undo_ttl_s(默认 7 天,-1 永久)清理撤销副本;缺失文件的快照在受控的 openStore 中清理;损坏的库会被隔离并自动重建。 central 的 janitor(apply + agent/created 受控节流 >24h)会先清理 mtime>storeMaxAgeS(默认 2592000 秒 = 30 天),再按 LRU 至 count<100 && sum<500MB,永不删除存活的 hash(workspaceCwd),并在关闭时执行 wal_checkpoint(TRUNCATE)。DB 文件为可丢弃缓存——可安全删除,下次 read 时重建。

项目结构

dsh-better-edit/
├── src/
│ ├── hashline/ # 哈希 + 已提供状态核心(从 pi-hashline-edit-lsz 逐字节移植) │ ├── tool-read.ts # read — HASH│内容、offset/limit 分页 │ ├── tool-edit.ts # edit — 按哈希范围、reject-and-serve │ ├── tool-batch-edit.ts │ ├── tool-undo.ts # undo_last_edit │ ├── sandbox.ts # FsSandboxController 镜像(sandbox_permissions/justification) │ ├── write-hook.ts # 附加到 write 结果的自动读取 │ ├── served-store.ts # 按工作区的 SQLite 存储(node:sqlite) │ └── workspace.ts # 会话 cwd 的 AsyncLocalStorage 载体 ├── benchmark/ # 可复现的 hashline、str_replace 与 oh-my-pi token 基准测试 │ └── corpus/ # 固定的 103 行语料 ├── test/ # 615 个测试(移植 + 回归) ├── assets/ # logo 与 banner ├── cordis.patch.yml # bundle 补丁 └── package.json # dsh.bundle manifest

开发

npm install
npm run typecheck # tsc --noEmit
npm test # vitest run(615 个测试)
npm run build # tsc → lib/
npm run benchmark # 可复现的 token 成本基准测试(benchmark/)

发布流程(先打 tag)

npm run release -- 0.2.0 # 升版本 + 迁移 CHANGELOG + 提交 + 打 tag + 推送 → 生成 GitHub release
npm publish --registry https://registry.npmjs.org # 版本未打 tag 前会被阻止

npm run release 会更新 package.json/lockfile、把 CHANGELOG 的 [Unreleased] 段落迁移到版本号下、提交、打 vX.Y.Z tag 并推送——tag 推送会基于 changelog 自动创建 GitHub release。npm publish 在该 tag 存在之前会拒绝运行(prepublishOnly 门禁),因此每个 npm 版本都一定已经打好 tag 并发布过 release。

测试套件移植自 pi-hashline-edit-lsz,通过本地文件系统桥接直接驱动 dsh 工具构建器。

路线图

当前状态(1.0.0): v2 行身份架构、按会话 (session,path) 存储、1648 个测试、中英双语 README、dsh 0.2.1-rc.2。

下一步

- 缩小或证明与 @oh-my-pi/hashline 的差距(参考:../oh-my-pi.md)。这个兄弟补丁语言负载更轻——基准测试中相对 str_replace 省 42%/53%,而我们省 31%,因为它裸文本式的补丁文档跳过了我们每次调用都要付的 JSON 外壳——还提供了我们不支持的四种能力:语法块操作(PUT N*:)、寄存器 + REM/MV、一次变更一个多 hunk 文档、可插拔文件系统。代价在正确性一侧:它的行号未经验证(当前标签下的错行号会静默落盘)、每次编辑都要重新编号、过期标签触发尽力而为的三方合并而非校验、语法也抬高了模型的技能门槛。逐项决定是拒绝还是采纳——负载差距本身不足以成为切换格式的理由。
- 在 dsh 会话中实测 1.0.0(0.2.1-rc.2 运行时)。
- 把 served-tail 截断修复回馈给 pi-hashline-edit-lsz / 上游(他们的 upsertServed 同样从不截断)。
- 对照下一个 dsh 版本重新核对插件接线(当前固定在 0.2.1-rc.2;dsh 处于开发者预览阶段,承诺会有破坏性变更)。

贡献

见 CONTRIBUTING.md(或者直接开 issue)。当前最有价值的贡献是更多基准场景和针对已提供状态校验的边界测试。

许可证

MIT License——详见 LICENSE。移植自 pi-hashline-edit-lsz(MIT),其本身带有 RimuruW 与 YuGiMob 的上游版权声明。

致谢

哈希锚定编辑源于 Can Bölük 的 _The Harness Problem_——那篇文章证明了瓶颈在于 harness 而非模型,并证明锚定编辑优于搜索替换。本项目站在以下巨人的肩膀上:

- pi-hashline-edit(RimuruW)——引入 3 字符哈希与冲突消解的原创 pi-coding-agent 扩展。
- pi-hashline-edit-pro(YuGiMob)——本仓库 hashline 核心所移植自的加固版 fork。
- pi-hashline-edit-lsz——本项目所跟随的自维护 fork。hashline 核心逐字节移植;工具层基于 dsh 的插件 API 重写。

延伸阅读:Hash anchors + Myers diff + single-token anchors(dirac.run)(关于编辑调用 O(S+R) → O(R) 节省的设计评论)以及一个独立的 hashline 与 replace 对比基准测试。

Star History

⭐ 如果 hashline 编辑让 Agent 的编辑更可靠,就给它一个 star 吧!
预览分享卡片长什么样 ↓Rianico/dsh-better-edit
分享 · DeepSeek Harness Hub
Rianico/dsh-better-edit

一个专为 DeepSeek Harness 而生的更好的编辑工具。

回到顶部