跳到主要内容

dsh-hashline-edittool

已验证

dsh-hashline-edittool · v0.10.1 · MIT · Web 界面

Line-anchored read/edit/batch_edit/grep/undo_last_edit tools for DeepSeek Harness (dsh). Every line is addressed by `<line>:<anchor>` so chained edits skip a re-read; stale or ambiguous anchors are rejected and a fresh `<line>:<anchor>` is served back. Un

安装

dsh plugin add dsh-hashline-edittool

用 dsh --profile default --dump-config 确认 layer 已生效 —— 参见安装指南。

源码

标签

作者

说明文档

dsh-hashline-edittool

hashline 卡片:读 / diff / grep / LSP / 设置

DeepSeek Harness 的行锚定编辑工具
每一行都有一个变长内容锚点 —— 不写行号、不回抄旧代码,更省 token,把上下文留给真正的工作。

English · 简体中文

快速开始 • 锚点契约 • 工具 • 设置 • 错误码 • 架构 • 致谢

MIT License DeepSeek Harness Plugin npm version GitHub Stars


它是什么

一个 DeepSeek Harness 插件:用哈希锚定版本替换内置的 read / edit / grep 工具,并在此基础上提供 undo_last_edit、ast_grep、ast_edit 和 lsp 工具:

  • 每一行都携带内容锚点 —— 变长 Base62 标记(前 3,844 行只需 2 个字符,随文件规模 才会增长)。模型按标记编辑,永远不需要回抄要替换的代码。
  • 编辑会对照模型实际看到的内容做校验。 每个锚点都逐行校验:必须仍然存活、它绑定的那一行 必须仍持铸锚时的内容、且它必须在本会话的 served 集合里。行在会话期间被改动会被拒 —— 而拒绝信息会回显失效行并附带真锚点、可直接使用(reject-and-serve)。文件校验和变化是 重映射信号而不是拒绝理由,所以别的会话改了别的行,永远不会让你的锚点失效。
  • 同一工作区内锚点跨会话共享。 在一个会话里读过文件,另一个会话可以直接改,不必重读: 锚点是每工作区的身份,而「这一行能不能写」按会话回答。若另一个会话替换了你手里那一行, 你会拿到拒绝与该行当前的锚点 —— 而不是静默写到别处。
  • 每个文件一个原子批次。 同一 edit 调用里的所有锚点都对照原始快照解析,因此每一项都用原始锚点。 单个文件内的编辑 all-or-nothing:一条失败即拒绝该文件的批次([E_BATCH_ABORT])、该文件什么都不写; 文件之间互不牵连 —— 多文件调用逐文件上报部分成功,每个失败各自带错误码。
  • 一切皆卡片。 随包的 client 插件从结构化 presentationMeta 在 dsh web UI 渲染 读 / diff / grep / 撤销 / 写入 / 结构 / LSP 卡片 —— 模型文本与 UI 永远不需要靠字符串 解析达成一致。

以单个 npm 包(dsh-hashline-edittool)交付:宿主插件 + web 卡片插件 + prompt sections, 由一个 bundle patch 挂载。

亮点

自渲染卡片。 随包 client 插件自带 React 组件——HashlineReadRow、HashlineEditRow、HashlineGrepRow(文件 tab + 命中高亮)、HashlineUndoRow、HashlineWriteRow、HashlineAstGrepRow、HashlineAstEditRow、HashlineLspRow——直接注册进 dsh web UI 的插槽。每张卡片从工具的结构化 presentationMeta 渲染,锚点 gutter、diff 行、高亮 span 全部原生绘制:没有通用工具输出卡,没有字符串互解,不改上游 web。

动态长度锚点。 锚点不是定宽哈希。分配最短优先:2 个字符覆盖前 3,844 行,只有文件真需要时才长出新层(上限 62⁸ 行,实际不可达)。全数字编码被跳过,标记永远不会与行号混淆;冲突在分配时探测消解;会话内编辑后幸存的行保持原锚点——分配/释放走统一生命周期门,重写与外部变更按行对齐继承。

AST + LSP 双语义后端。 ast_grep / ast_edit 通过沙箱化的 tree-sitter worker 回答「语法在哪里匹配」,配精选语法目录(SHA-256 校验下载、安装/卸载路由)与长文件的可编辑折叠大纲。lsp 工具通过每语言一个的真实语言服务器回答「这个符号是什么」——按需启动,与 dsh 自带 lsp 服务共享——起不了服务器时降级启发式后端。两者都会 serve 自己的行,结构与语义结果可直接编辑。

写入即诊断(自动诊断)。 编辑/写入落盘后,插件以写入前的文本为基线通知语言服务器,拉取诊断并按写入文件逐一投递——短窗口内联胶囊挂在卡片下方(严重度着色),未竟部分在模型下一步自然边界异步补投;JSON 信封与 text 模式双通道可达。lsp.auto_diagnostics 一键关闭。

插件自渲染的设置面板。 随包的 HashlineSettingsCard 是 web 端完整设置 UI:分隔符、输出格式、上下文行数、行号(模型侧标记形态,默认关)、require_line_content、AST 总开关与按语言开关、命名 LSP 服务器、自动诊断。改完提交即生效——不用碰 YAML。

处处热切换。 提交的设置改动下一次工具调用即生效——输出格式、分隔符、上下文行数、AST/LSP 开关(已实测:会话中途切换 output_format,模型收到的内容立即改变)。影响面最大的开关也有处理:切换 require_line_content 会销毁并重挂 edit 工具的 schema,模型下一步就看到新的 { anchor, line } 参数集——全程无需重启。

快速开始

npx @deepseek-ai/dsh plugin --profile web add github:hyperion2144/dsh-hashline-edittool   # 从 github
npx @deepseek-ai/dsh plugin --profile web add dsh-hashline-edittool                       # 从 npm
npx @deepseek-ai/dsh plugin --profile web add /path/to/dsh-hashline-edittool              # 本地检出

该 profile 的下一个会话即装即用。验证层已挂载:

dsh --profile <name> --dump-config   # 会出现 "# == dsh-hashline-edittool" 层
要求
Node ^22.19.0 || >=24.0.0(dsh 的要求;存储使用 node:sqlite)
Profile 一个 dsh profile(首次使用 dsh plugin 会自动初始化)
后端 支持沙箱 / 远程文件系统(写入走 ctx.fs)

锚点契约

标记

  • 锚点是变长 Base62 标记,每行唯一(内容相同的行拿到不同的锚点 —— 锚点是行身份, 不是可以猜的内容哈希)。全数字的编码会被跳过,所以标记永远不会是纯数字。
  • 标记写作 <anchor> 或 <anchor>:<line>;line 只是位置提示 —— 锚点才是权威, 提示与锚点不一致只给警告([E_LINE_HINT]),不是错误。旧的 <line>:<anchor> 顺序 仍然接受。
  • read 输出以 ANCHOR:FILELINE 头开始,分隔标记列与逐字内容,使用配置的分隔符 (下例为 |):
ANCHOR:FILELINE
G8:1|// UI 演示文件
ur:2|export const APP = "hashline";
D0:4|export function greet(name: string): string {

已读状态校验(reject-and-serve)

工具结果向模型展示过的行即成为 served(read、grep、编辑 diff、结构结果、LSP 行)。锚点就在那一刻、经同一个入口铸出,且铸锚与 served 记录同一事务提交 —— 模型看见过的锚点,必定是某个会话「见过」的锚点,下面的校验依赖的正是这一点。

分配发生在响应被截断之后。 被响应预算切掉的行是模型从未见过的行,因此既没有锚点 也不进 served;等后续调用真的展示它时才铸。所以「我手里有标记」始终意味着「我见过那一行」。

edit 对每个锚点逐行校验,三条同时成立才算可写:

  1. 锚点存活 —— 在该文件的锚点状态里,且它绑定的那一行当前内容键与铸锚时一致;
  2. 锚点在本会话的 served 集合里;
  3. 调用方申报的行号只是信息来源 —— 不一致按漂移提示,绝不因此拒绝。

文件校验和变化本身不是拒绝理由。 它是「文件被别处改动过」的信号,触发一次 重映射:内容未变的行带着原锚点移到新行号,只有内容真的变了的行才被释放并重新铸锚。 若因为校验和就拒,那么别人碰过文件任何一处之后,我这一行就不可编辑了 —— 而跨会话共享 锚点存在的意义恰恰是避免这件事。

释放一个锚点会同时清三处:它不再存活、它离开发起释放的那个会话的 served 集合、 它进入那次调用的释放池,使同一次调用无法把它发给另一行。少做任何一处都会留下那个 静默错行窗口:一个能解析、能校验通过、却指向别处的锚点。

失败是全有或全无的,错误码集合未变:

  • 锚点未知 → [E_STALE];区间行在本会话从未 served → [E_RANGE_UNVERIFIED];
  • 文件校验和变化 → [E_RANGE_STALE],是重映射的信号而非拒绝。宿主的 FS_STALE_VERSION (写入被文件版本守卫拦下)也报同一个码,因为补救手段相同(重新 read 拿新锚点); 只有前者是重映射信号;
  • 每次拒绝都会回显失效行及其少量上下文,作为带真锚点的 served 行 —— 修复方式就是:取回显里 的标记重新提交。served 行同时以 fs/observed 发出,立即可写。

没有 Shift: 块 —— 编辑之后,从响应刚给出的 diff 行取锚点,或者重新 read。

批量语义

  • edits[] 按序作用于同一快照;范围重叠是 [E_BATCH_CONFLICT];该文件批次内任何失败是 [E_BATCH_ABORT],该文件一个字都不写。
  • 使用逐条 path(或每条都带 path)时,条目按文件分组,每个文件独立 all-or-nothing;结果聚合为 success[] / fail[](多文件形态)。 item.path === topLevelPath 自动折叠为缺省。
  • 部分失败现在是可见的:一个文件成功、另一个失败时,这次调用不算失败 —— 每个失败文件 各自带错误码与补救提示,网页卡片给出 N of M files failed 横幅,并为每个失败文件开一个 tab(tab 条上标出)。聚合的 [E_BATCH_ABORT] 文案、连同它的「什么都没写」,只属于 每个文件都失败的调用。
  • 每次调用最多 32 条编辑。

op 语义

op 锚点字段 行为
replace anchor_start(+ 可选 anchor_end) 用 lines 换掉该范围(非空;一个元素就是一行 —— 只有元素自身含换行符才添行;[""] 把行清成空行,区别于 del)。省略 anchor_end = 单行替换;范围跨多行时必填。
ins anchor_after 在该行下方插入 lines —— 锚点行保留,lines 只放新内容。anchor_start/anchor_end 会被拒绝。可以锚在其他 hunk 范围的结束行,绝不能是起点或内部。
del anchor_start(+ 可选 anchor_end) 删除范围(或单个 anchor_start 行);lines 被忽略。
sed anchor_start(+ 可选 anchor_end) 用 pattern + replacement + 可选 flags(gims)逐行重写范围;不放 lines,replacement 不得含换行;sed 的 \1/& 与 JS 的 $1/$& 都接受。

锚点字段与 op 不匹配是 [E_BAD_SHAPE]。

require_line_content(可选加固)

开启 hashline.require_line_content 后,每个锚点变成 { anchor, line } 对 —— line 是你对该行当前完整文本的声明。声明在陈旧锚点检查之后验证;不匹配则以 [E_CONTENT_MISMATCH] 拒绝该文件的批次,并回显你声明的内容实际所在的位置。

grep_respect_gitignore(默认开)

grep 的候选文件清单由 ripgrep 出,因此 .gitignore / .ignore 规则决定「什么会被搜」: 构建产物与 vendored 副本根本不进读取阶段(自然也不吃那 64 MiB 读取预算)。只认被搜目录 自身的忽略规则 —— 你点名一个被忽略的目录,它照样会搜。关掉 hashline.grep_respect_gitignore 即退回插件自己的走查(整棵树减去隐藏项与 node_modules),rg 无法解析时也是同一回退。

工具

工具 功能
read 文件即 served 行:ANCHOR:FILELINE 头 + <anchor>:<line> 标记,或裸 <anchor> —— 形态由用户的 line_numbers 设置决定(默认关),不再是工具参数,仍带该参数即 [E_BAD_SHAPE]。offset 收 1 起的行号或锚点(含该行;纯数字字符串即该行号的文本形态,按行号解析,limit 的行数同理);limit 收行数或锚点(含该行)—— 关掉行号后,锚点是你唯一能看到的游标。text 模式的每个窗口都以一句话收尾:正文被截断时是 [Lines X-Y of N. Omitted K lines. Use read {resume: "TOKEN"} to continue.],否则 [Lines X-Y of N. Use offset="ANCHOR" to continue.](纯行号游标则是 offset=N),读到底则 [Lines X-Y of N. End of file.]。超长行(>200KB)变成标记 + sed 提示 —— 锚点需要完整行。
edit 通过 { path?, edits: [{ op, … }, …] } 应用一或多条范围编辑 —— 完整契约见上节。取代旧的 batch_edit。
write 完全影子化:创建/覆盖文件,返回写入结果外加带新鲜锚点的自动 read 预览,下一次编辑不再需要单独 read。
grep JavaScript 正则搜索(regex: false 为字面量),跨路径树逐文件一节、同一表头,只输出完整行。清单由 ripgrep 出,因此 .gitignore 的路径默认跳过(见 grep_respect_gitignore)。-C N 回显上下文行;命中即 served → 可直接编辑。
undo_last_edit { path } 撤销该文件最后一次 hashline 编辑 —— 仅当文件仍与存储的编辑后内容一致时;可跨重启。
ast_grep 按语法形状结构搜索(pat 使用 $NAME / $$$ARGS / $_ 元变量)。模式无法解析为单一节点时拒绝而不是猜测。长文件返回可编辑的折叠大纲。
ast_edit 结构改写:按形状找到位置,把变更交给 edit 同一引擎 —— served 校验、undo 记录、diff 与语法门全部生效。
lsp 能起语言服务器就做符号级工作(按语言启动,与 dsh 的 lsp 服务共享);否则降级启发式后端。其行会被 serve,因此 LSP 输出可直接编辑。

输出模式

hashline.output_format 切换面向模型的文本:

  • text(默认)—— 上文的 ANCHOR:FILELINE 行格式;
  • json —— 纯 JSON 信封(如 edit 返回 { ok, path, diff, hints, warnings }, diff 是 {"<锚点>:<行>": 内容} 字典)。结构化,适合偏好解析的模型。

web 卡片不受影响 —— 它们从 presentationMeta 渲染,永远结构化。

设置

所有键是插件自身的 Profile 配置(dsh ≥ 0.1.7):在网页端 设置 → 插件 → dsh-hashline-edittool(行页面的配置表单)里编辑并提交。所有改动热更新:下一次工具调用即生效,无需重启。同一份值的原生编辑视图是 profile 补丁($DSH_HOME/profiles/<名>/cordis.patch.yml)中本条目的 config: 块;全部键可选:

- id: dsh-hashline-edittool
  config:
    separator: "|"           # 标记/内容列分隔符(默认 ":")
    output_format: text      # "text" | "json"
    context_lines: 3         # 陈旧回显 / diff 的上下文行数(0..20)
    max_response_chars: 48000 # 每次响应的字符预算(默认 48000,范围 [8000, 49984])
    require_line_content: false
    line_numbers: false     # 模型侧行标记:`<anchor>:<line>`(true)或裸锚点(false)
    ast:
      enabled: true
      languages: {}          # 按语言收窄:{ <id>: { enabled: false } }
    lsp:
      servers: {}            # 命名服务器:{ <languageId>: <command> }
      auto_diagnostics: true # 写入后内联投递服务器诊断

从 0.1.6 迁移?旧 ~/.dsh/settings.yaml 的导入由 dsh 自己在升级时完成;本插件不再读取、迁移或写入任何设置文件——本页的条目配置是唯一真相源。旧的 hashline: 键若没带过来,在上面的表单里补上即可。

按 preset 配置指引

tool:read / tool:edit / tool:grep / tool:undo_last_edit 指引段是插件共享目录里 按 preset id 存放的纯 markdown 覆盖文件 —— 见 docs/adr/0001。清空文件即重置为编译默认; front-matter 围栏损坏会快速失败并告警。

错误码

代码 含义
[E_ACCESS] 文件存在但不可读/不可写。
[E_BAD_OP] 范围终点在起点之前(方向颠倒时自动纠正)。
[E_BAD_REF] 锚点字段不是从行首列复制的标记。
[E_BAD_SHAPE] 请求/字段形状错误(未知字段、op 与锚点字段不匹配等)。
[E_BATCH_ABORT] 批内一条失败;该文件的批次被拒绝,该文件什么都没写。
[E_BATCH_CONFLICT] 两条目在同一快照上范围重叠。
[E_CONTENT_MISMATCH] (require_line_content)声明的 line 不匹配。
[E_ELISION_IN_PAYLOAD] 载荷携带大纲标记 …;警告,编辑继续。
[E_HASH_SPACE] 锚点空间耗尽(> 62⁸ 行)。
[E_INS_ANCHOR_DUP] ins 的 lines[0] 与锚点行重复;警告,继续。
[E_INVALID_PATCH] diff 预览标记被粘进 lines;剥除并警告。
[E_LINE_HINT] <line>:<anchor> 提示与锚点不一致;以锚点为准。
[E_LINE_REF] 锚点字段传了纯数字;安全时按 served 状态解析。
[E_NOOP_LOOP] 同一编辑反复无变化;再提交被拒绝。
[E_NOT_FOUND] / [E_NOT_TEXT] 文件不存在 / 目录-二进制-非 UTF-8。
[E_NOT_OBSERVED] 本会话未观察过该文件(先读后写策略)。
[E_OP_INS] 提示:ins 已把行插入锚点之后。
[E_PASTE_DUP] 替换行与相邻文件行相同;原样保留。
[E_RANGE_STALE] / [E_RANGE_UNVERIFIED] served 校验失败;范围已回显为新鲜行。E_RANGE_STALE 也是宿主 FS_STALE_VERSION 的报码:补救相同(重新 read),成因不同。
[E_RESUME_CONFLICT] 一次 read 同时带了 resume 令牌与 offset/limit;令牌已经指明窗口,二者只能给一个。
[E_RESUME_GONE] / [E_RESUME_BAD] / [E_RESUME_TOOL] resume 令牌的 spill 已消失或过期 / 令牌形状错误、内容损坏或属于别的会话 / 必须由另一个工具消费。
[E_STALE] 锚点不再匹配 served 内容;重新 read。
[E_SYNTAX_AFTER_EDIT] ast_edit 的替换会让文件无法解析;未写入。
[E_UNDO_STALE] / [E_UNDO_UNAVAILABLE] 编辑后文件被改动 / undo 历史无法持久化。
[E_WOULD_EMPTY] 编辑会把非空文件清空;请用 write。
[E_WIN_REPLACE] Windows 原子替换被其他进程占用。
[E_AST_DISABLED] / [E_AST_PATTERN] / [E_AST_TOO_LARGE] / [E_AST_WORKER_ABORTED] / [E_AST_WORKER_FAILED] / [E_PARSE_FAILED] AST 能力:按语言关闭 / 模式无法解析为单一节点 / 超过 AST 大小上限 / worker 中止 / worker 失败 / 文档解析失败。
[E_GRAMMAR_BUILTIN] / [E_GRAMMAR_NO_DESCRIPTOR] / [E_GRAMMAR_UNKNOWN] 语法目录:内置名冲突 / 该语言无描述符 / 未知语言。
[E_GRAMMAR_FETCH_FAILED] / [E_GRAMMAR_HASH_MISMATCH] / [E_GRAMMAR_NOT_IN_TARBALL] 语法下载失败 / SHA-256 不匹配 / tarball 中缺少条目。
[E_LSP_NO_SERVER] / [E_LSP_BAD_OPERATION] / [E_LSP_UNAVAILABLE] / [E_LSP_ABORTED] / [E_LSP_CLOSED] / [E_LSP_NOT_READY] / [E_LSP_TIMEOUT] LSP:该语言无服务器 / 操作无效 / 服务器不可用 / 请求中止 / 通道已关闭 / 仍在启动 / 超时。
[E_BARE_HASH_PREFIX] lines 中粘入了带锚点前缀的行;剥除并警告。
[E_ANCHOR_AMBIGUOUS] 锚点同时活在多行上(被释放的锚点在模型仍持旧绑定时被重新分配)——拒绝;请重新 read。该文件未写入任何内容。

存储

锚点身份、served 行与 undo 历史保存在按工作区键控的一个 SQLite 存储中:

$DSH_HOME/plugins/dsh-hashline-edittool/<projectKey>/hash-store.sqlite

<projectKey> 是会话 cwd 的人类可读编码,并行工作区之间永不共享锚点与 undo 历史。 工作区之外的调用方回落到共享主目录存储。served 行按 7 天 TTL 清理;损坏的存储自动 隔离重建。

架构

一个插件,三个平面:

src/
├── index.ts              # 入口:挂载工具、设置、LSP、语法路由
├── config.ts             # 设置 schema + 接线(热更新)
├── tools/                # 8 个工具入口 —— 薄壳,不做 IO
├── domain/
│   ├── edit/             # 编辑引擎、变更事务、契约、prompts
│   └── session/          # served 状态、hash store、文件视图
├── render/               # 按卡片的投影:读 / 编辑 / grep 卡、diff 渲染器
├── contract/             # 请求形状 + 校验(schema 是唯一权威)
├── hashline/             # 锚点核心:分配、resolve/apply 引擎
├── infra/                # fs 桥、沙箱、路径、设置快照、工作区作用域
├── lsp/                  # 语言服务器会话、自动诊断
├── ast/                  # tree-sitter worker、语法注册表
└── guidance/             # 按 preset 的指引解析 + 物化
client/                   # web 卡片插件(同一包)
test/                     # 1,330 个测试

依赖只指向一个方向:tools → domain → render/contract → hashline/infra。卡片从结构化 presentationMeta 渲染;模型文本与 UI 永远不互相解析。领域词汇表见 CONTEXT.md,契约背后的决策见 docs/adr/。

DSH 版本支持

兼容性通过对等依赖声明(@deepseek-ai/schemastery >=3.18.3, npm 强制),并在本仓库实际运行的 harness 上验证:

dsh 版本 插件版本 说明
0.1.7-alpha.1(当前构建/测试 SDK 基线) 0.9.x 设置即插件的 Profile 配置(dsh ≥ 0.1.7),下一次工具调用即热生效;稀疏惰性锚点;有界锚点存储
0.1.6-alpha.1(前一 SDK 基线) 0.7.1 – 0.8.x 双事件注册(agent/created + 旧 agent/session-start);systemPrompt 按作用域服务解析
0.1.5-rc.2(实测通过) 0.7.0 统一锚点生命周期、AST/LSP 拆分、设置卡片、卡片全景图
≥ 0.1.2-rc.0 0.6.x – 0.7.1 自渲染卡片、按工作区存储、设置面板
0.1.2 0.4.x – 0.5.x v2 动态锚点;dsh 0.1.2 web 卡片适配完成(#69)
0.1.2(早期) 0.1.x – 0.3.x 旧 line#hash 锚点、batch_edit
  • 构建/测试 SDK 基线:0.1.7-alpha.1(0.9.0 起,#134);0.9.x 要求 dsh 0.1.7,0.8.x 留给 0.1.6。0.7.0 线在 dsh 0.1.5-rc.2 上验证。
  • 更新的 dsh 0.1.x/rc 线预期可用;发现回归请提 issue。

开发

npm run typecheck   # tsc --noEmit(src + test 两个工程)
npm test            # vitest
npm run build       # 清理 lib/ + tsc + client 工作区

发布是先打 tag:npm run release -- X.Y.Z 升版本、移动 changelog、打 vX.Y.Z 标签 并推送 —— tag 触发 GitHub Actions 发布流程。tag 存在之前 npm publish 会被阻止。PR 优先;正文写 Closes #NN。见 .agents/skills/git-std.md。

许可证

MIT

致谢

本项目 fork 自 Rianico/dsh-better-edit,此后独立维护 —— 感谢 @Rianico 打下的基础,以及把哈希锚定编辑带给 DeepSeek Harness 用户。

那个 fork 本身也站在 hashline 谱系之上,本项目一并感谢:

  • pi-hashline-edit(RimuruW)—— 引入 内容哈希与冲突消解的原创 pi-coding-agent 扩展;
  • pi-hashline-edit-pro(YuGiMob) —— 本仓库 hashline 核心所移植自的加固版 fork;
  • Can Bölük 的 The Harness Problem —— 证明了瓶颈在 harness 而非模型的那篇文章。

延伸阅读:Hash anchors + Myers diff + single-token anchors (dirac.run) 与独立的 hashline 与 replace 对比基准。


Star History

Star History Chart


⭐ 如果 hashline 编辑让 Agent 的编辑更可靠,就给它一个 star 吧!