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

哈希锚点编辑 hyperion2144/dsh-hashline-edittool

DeepSeek Harnessv0.10.1
hyperion214428 天前有提交已提供中文文档扫描:含敏感能力

DeepSeek Harness 的行锚定编辑工具每一行都有一个变长内容锚点 —— 不写行号、不回抄旧代码,更省…

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

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

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add dsh-hashline-edittool
npm 源 · dsh-hashline-edittool 的发布者账号已校验与本仓库一致

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · security
装得上吗
本站已在 dsh 0.2.1-rc.2 下真实安装成功
非静态推断
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 28 天前
档位由哪些信号合成

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

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

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

✓npm 包dsh-hashline-edittool @ 0.10.1
✓Node 引擎要求 ^22.19.0 || >=24.0.0 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/12 11:38:55

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

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

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 92 处,展示前 90 条)
  • 发起网络请求
    client/lib/client.js:4426
    const response = await fetch(`${MANAGER_ROUTE}/updates`, { headers: { accept: "application/json" } });
  • 发起网络请求
    client/lib/client.js:4610
    const response = await fetch(MANAGER_ROUTE, { headers: { accept: "application/json" } });
  • 发起网络请求
    client/lib/client.js:4641
    const response = await fetch(LSP_ROUTE, { headers: { accept: "application/json" } });
  • 发起网络请求
    client/lib/client.js:4766
    const response = await fetch(`${LSP_ROUTE}/install`, {
  • 发起网络请求
    client/lib/client.js:4809
    const response = await fetch(`${MANAGER_ROUTE}/${action}`, {
  • 写入本地文件
    lib/infra/fs-bridge.js:413
    await rename(fromProc, toProc);
  • 删除本地文件
    lib/infra/fs-bridge.js:502
    await unlink(proc);
  • 读取本地文件
    lib/infra/fs-bridge.js:527
    return readFile(absolutePath, "utf-8");
  • 读取本地文件
    lib/infra/fs-bridge.js:534
    info = await lstat(absolute);
  • 读取本地文件
    lib/infra/fs-bridge.js:564
    const bytes = await readFile(absolutePath);
  • 读取本地文件
    lib/infra/fs-bridge.js:592
    const st = await stat(absolutePath);
  • 删除本地文件
    lib/infra/fs-bridge.js:629
    await unlink(before.path);
  • 读取本地文件
    lib/infra/fs-bridge.js:665
    stat(before.path, { bigint: true }),
  • 读取本地文件
    lib/infra/fs-bridge.js:666
    stat(dest.path, { bigint: true }),
  • 写入本地文件
    lib/infra/fs-bridge.js:677
    await rename(before.path, dest.path);
  • 读取本地文件
    lib/tools/tool-grep.js:344
    const rootStat = await lstat(root);
  • 发起网络请求
    lib/tools/tool-lsp.js:512
    const answer = await session.request(method, params);
  • 发起网络请求
    lib/lsp/provider.js:71
    const raw = await session.request(METHOD_FOR[request.operation], {
  • 读取本地文件
    lib/lsp/provider.js:96
    return pathToFileURL(await realpath(workspaceRoot)).href;
  • 读取本地文件
    lib/lsp/discovery.js:208
    const info = await stat(path);
  • 读取本地文件
    lib/lsp/discovery.js:216
    await access(path, constants.X_OK);
  • 读取本地文件
    lib/infra/paths.js:118
    const candidateStats = await lstat(candidatePath);
  • 读取本地文件
    lib/infra/paths.js:128
    const linkTargetPath = resolvePath(dirname(candidatePath), await readlink(candidatePath));
  • 读取本地文件
    lib/infra/fs-write.js:15
    const entries = await readdir(dir, { withFileTypes: true });
  • 读取本地文件
    lib/infra/fs-write.js:22
    const stats = await stat(tempPath);
  • 删除本地文件
    lib/infra/fs-write.js:24
    await rm(tempPath, { force: true });
  • 读取本地文件
    lib/infra/fs-write.js:53
    existingStats = await stat(targetPath);
  • 写入本地文件
    lib/infra/fs-write.js:61
    await writeFile(targetPath, content, "utf-8");
  • 写入本地文件
    lib/infra/fs-write.js:67
    await mkdir(dir, { recursive: true });
  • 删除本地文件
    lib/infra/fs-write.js:79
    await rm(tempPath, { force: true });
  • 写入本地文件
    lib/infra/fs-write.js:86
    await rename(tempPath, targetPath);
  • 写入本地文件
    lib/infra/fs-write.js:92
    await writeFile(targetPath, content, "utf-8");
  • 删除本地文件
    lib/infra/fs-write.js:97
    await rm(tempPath, { force: true });
  • 删除本地文件
    lib/infra/fs-write.js:103
    await rm(tempPath, { force: true });
  • 读取本地文件
    lib/infra/file-snap.js:32
    const stats = await fsStat(canonicalPath);
  • 读取本地文件
    lib/domain/session/file-view.js:154
    const pathStat = await fsStat(filePath);
  • 读取本地文件
    lib/domain/session/file-view.js:228
    await fsAccess(absolutePath, accessMode);
  • 读取本地文件
    lib/domain/session/file-view.js:261
    const stats = await fsStat(canonicalPath);
  • 删除本地文件递归
    lib/infra/response-stream.js:108
    rmSync(dir, { recursive: true, force: true });
  • 读取本地文件
    lib/infra/response-stream.js:119
    for (const entry of await readdir(SPILL_ROOT)) {
  • 读取本地文件
    lib/infra/response-stream.js:123
    const info = await stat(child).catch(() => undefined);
  • 删除本地文件递归
    lib/infra/response-stream.js:125
    await rm(child, { recursive: true, force: true }).catch(() => undefined);
  • 读取本地文件
    lib/infra/response-stream.js:128
    for (const entry of await readdir(dir)) {
  • 读取本地文件
    lib/infra/response-stream.js:131
    const info = await stat(join(dir, entry)).catch(() => undefined);
  • 删除本地文件
    lib/infra/response-stream.js:134
    await rm(join(dir, entry), { force: true }).catch(() => undefined);
  • 删除本地文件
    lib/infra/response-stream.js:135
    await rm(join(dir, `${base}.rows.json`), { force: true }).catch(() => undefined);
  • 写入本地文件
    lib/infra/response-stream.js:146
    await mkdir(dir, { recursive: true });
  • 写入本地文件
    lib/infra/response-stream.js:151
    await writeFile(join(dir, file), JSON.stringify({ rows: opts.rows }), "utf-8");
  • 写入本地文件
    lib/infra/response-stream.js:164
    await writeFile(join(dir, `${token}.res.json`), JSON.stringify(sidecar), "utf-8");
  • 读取本地文件
    lib/infra/response-stream.js:175
    raw = await readFile(path, "utf-8");
  • 读取本地文件
    lib/infra/response-stream.js:198
    const data = JSON.parse(await readFile(path, "utf-8"));
  • 写入本地文件
    lib/infra/response-stream.js:206
    await writeFile(path, JSON.stringify(sidecar), "utf-8");
  • 写入本地文件
    lib/domain/session/hash-store.js:1131
    await rename(candidate, `${candidate}${suffix}`);
  • 读取本地文件
    lib/domain/session/hash-store.js:1208
    await stat(row.path);
  • 写入本地文件
    lib/domain/session/hash-store.js:1243
    await mkdir(dirname(storePath), { recursive: true });
  • 读取本地文件
    lib/domain/session/hash-store.js:1244
    let existed = existsSync(storePath);
  • 读取本地文件
    lib/domain/session/hash-store.js:1571
    content = await readFile(legacyPath, "utf-8");
  • 写入本地文件
    lib/domain/session/hash-store.js:1608
    await rename(legacyPath, `${legacyPath}.bak`);
  • 读取本地文件
    lib/infra/file-scan.js:32
    entries = await readdir(dir);
  • 读取本地文件
    lib/infra/file-scan.js:43
    st = await lstat(p);
  • 读取本地文件
    lib/tools/grep-rg.js:65
    if (shipped !== undefined && existsSync(shipped))
  • 执行外部命令来自配置/变量
    lib/tools/grep-rg.js:166
    execFile(rg, args, {
  • 执行外部命令来自配置/变量
    lib/tools/grep-rg.js:451
    execFile(rg, [
  • 读取本地文件
    lib/ast/language.js:574
    return existsSync(grammarWasmPath(id));
  • 发起网络请求
    lib/lsp/session.js:195
    const report = (await this.request("textDocument/diagnostic", { textDocument: { uri } }, timeoutMs));
  • 写入本地文件
    lib/lsp/install-server.js:89
    await mkdir(prefix, { recursive: true });
  • 读取本地文件
    lib/lsp/install-server.js:189
    await access(executable, constants.X_OK);
  • 读取本地文件
    lib/ast/registry.js:136
    ids = await readdir(root);
  • 读取本地文件
    lib/ast/registry.js:144
    versions = await readdir(join(root, id));
  • 读取本地文件
    lib/ast/registry.js:151
    const raw = await readFile(join(root, id, version, "install.json"), "utf-8");
  • 写入本地文件
    lib/ast/registry.js:271
    await mkdir(dir, { recursive: true });
  • 写入本地文件
    lib/ast/registry.js:272
    await writeFile(join(dir, "grammar.wasm"), bytes);
  • 写入本地文件
    lib/ast/registry.js:281
    await writeFile(join(dir, "install.json"), `${JSON.stringify(facts, null, 1)}\n`, "utf-8");
  • 读取本地文件
    lib/ast/registry.js:306
    await stat(root);
  • 删除本地文件递归
    lib/ast/registry.js:311
    await rm(root, { recursive: true, force: true });
  • 读取本地文件
    lib/ast/registry.js:327
    await stat(installed.wasmPath);
  • 读取本地文件
    lib/guidance/resolve.js:92
    const content = await readFile(candidate, "utf-8").catch((error) => {
  • 读取本地文件
    lib/guidance/materialize.js:114
    content = await readFile(path, "utf-8");
  • 写入本地文件
    lib/guidance/materialize.js:124
    await writeFile(path, seededContent(section), { encoding: "utf-8" });
  • 写入本地文件
    lib/guidance/materialize.js:141
    await mkdir(homeDir, { recursive: true });
  • 写入本地文件
    lib/guidance/materialize.js:144
    await mkdir(dir, { recursive: true });
  • 读取本地文件
    lib/guidance/materialize.js:145
    const existing = new Set(await readdir(dir));
  • 写入本地文件
    lib/guidance/materialize.js:152
    await writeFile(path, seededContent(section), {
  • 读取本地文件
    lib/guidance/materialize.js:166
    const entries = await readdir(homeDir, { withFileTypes: true });
  • 读取本地文件
    lib/guidance/materialize.js:171
    const existing = new Set(await readdir(dir));
  • 读取本地文件
    lib/guidance/materialize.js:178
    const homeFiles = new Set(await readdir(homeDir));
  • 写入本地文件
    lib/guidance/materialize.js:186
    await writeFile(join(homeDir, file), content, {
  • 发起网络请求
    lib/lsp/format/requests.js:60
    return validateEdits(await session.request("textDocument/formatting", {
  • 发起网络请求
    lib/lsp/format/requests.js:80
    return validateEdits(await session.request("textDocument/rangeFormatting", {
  • 读取本地文件
    lib/ast/worker.js:75
    language = await Language.load(readFileSync(await resolveGrammarAsset(languageId)));
扫描于 2026/11/2(1 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

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

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

功能亮点

  • 哈希锚点读取
  • 撤销上次编辑

为 DeepSeek Harness(dsh)提供基于哈希锚点的读取、编辑、撤销上次编辑工具

v0.10.72026-11-02
· Added · README 增加 dsh-plugin.org 收录徽章(Listed on dsh-plugin.org)。 · 新增 screenshots.json:在插件仓库里声明市场截图,dsh-market 等店面按此展示。 · Changed · npm 包的 description 对齐当前工具集:去掉已在 0.4 契约中移除的 batch_edit,补上 write/grep/delete/rename/ast_grep/ast_edit/lsp/format,并把标记形态改成 (可选 : · )。 · Fixed · grep 的命中数与截断不再静默:每个文件一节末尾报该文件的真命中数与本次返回数,被每文件上限或响应预算截断时写明是哪一刀截的(续取段同样逐文件报);canonical total 改为真命中数(触到上限时补一趟不限额计数),并新增 truncatedBy(#386,ADR-0029)。 · grep 卡片折叠行标题尾显示命中数与截断原因(双语,与 read 行窗口同一槽位)(#386,ADR-0029)。 · LSP 自动诊断不再投递过期内容:同一文件的后续写入落地后,先前写入的延迟诊断会被丢弃,诊断探针也不再把旧文本回灌给语言服务器;已进入队列的旧报告带写入代际标注,与新报告自证先后(#388,ADR-0030)。 · 会话切换工作目录后,read/write/edit 等工具的相对路径跟随 DSH 的当前工作目录,不再落回会话创建时的旧检出(#394,ADR-0031)。
v0.10.62026-10-30
· Fixed · grep 字面模式(regex: false)搜目录不再恒返回「No matches」:模式作为一个参数原样传给 ripgrep,字面量改用独立的 --fixed-strings,不再把 -F 拼在模式文本前面(#384)。 · grep 改由 ripgrep 直接产出命中(rg --json),不再逐行用 JS 匹配;锚点、上下文行、预算续取、二进制保真等契约不变(#384,ADR-0028)。 · grep 的模式方言改为 ripgrep 正则(原先是 JavaScript 正则):ripgrep 拒收的模式即坏模式(E_BAD_SHAPE),与官方 grep 工具同口径(#384,ADR-0028)。
v0.10.52026-10-29
· Changed · 发布流程改走 npm 的 OIDC trusted publishing:推 tag 即由 CI 自动发包,不再需要本机浏览器 2FA,仓库里不存 npm token(#378)。 · edit 每次调用的编辑条数不再有上限:每条编辑各自寻址自己的行,一批有多长只取决于改动本身,不必再拆成多轮(#380)。
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。

README

由 DeepSeek 最新模型翻译生成
dsh-hashline-edittool DeepSeek Harness 的行锚定编辑工具每一行都有一个变长内容锚点 —— 不写行号、不回抄旧代码,更省 token,把上下文留给真正的工作。 English · 快速开始 • 锚点契约 • 工具 • 设置 • 错误码 • 架构 • 致谢 它是什么 一个 DeepSeek Harness 插件:用哈希锚定版本替换内置的 read / edit / grep 工具,并在此基础上提供 undo_last_edit、ast_grep、ast_edit 和 lsp 工具: - 每一行都携带内容锚点 —— 变长 Base62 标记(前 3,844 行只需 2 个字符,随文件规模才会增长)。模型按标记编辑,永远不需要回抄要替换的代码。 - 编辑会对照模型实际看到的内容做校验。 每个锚点都逐行校验:必须仍然存活、它绑定的那一行必须仍持铸锚时的内容、且它必须在本会话的 served 集合里。行在会话期间被改动会被拒 ——而拒绝信息会回显失效行并附带真锚点、可直接使用(reject-and-serve)。文件校验和变化是 重映射信号而不是拒绝理由,所以别的会话改了别的行,永远不会让你的锚点失效。 - 同一工作区内锚点跨会话共享。 在一个会话里读过文件,另一个会话可以直接改,不必重读:锚点是每工作区的身份,而「这一行能不能写」按会话回答。
展开完整文档(README 剩余 1 节)
dsh-hashline-edittool

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

English ·

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

它是什么

一个 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,部分成功会被明确上报。
- 一切皆卡片。 随包的 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 自动折叠为缺省。
- 每次调用最多 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> 标记(line_numbers: false 得到裸锚点)。offset(1 起)/ limit 分页;超长行(>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 ast: enabled: true
languages: {} # 按语言收窄:{ <id>: { enabled: false } }
lsp:
servers: {} # 命名服务器:{ <languageId>: <command> }
auto_diagnostics: true # 写入后内联投递服务器诊断

从 0.1.6 迁移?旧 ~/.dsh/settings.yaml 里的 hashline: 节会在插件首次启动时一次性自动导入本配置——除非你已在 0.1.7 上自行配置过(你的新值优先)。

按 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_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

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

DeepSeek Harness 的行锚定编辑工具每一行都有一个变长内容锚点 —— 不写行号、不回抄旧代码,更省…

这是你的仓库?认领条目

回到顶部