@flotiarenor/dsh-tool-text-editor 1.1.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.zh.md CHANGED
@@ -1,37 +1,23 @@
1
1
  # dsh-tool-text-editor
2
-
3
2
  中文 | [English](README.md)
4
3
 
5
- [DeepSeek Harness](https://github.com/deepseek-ai)(dsh)的模型工具插件,提供两个**字节保真**的
6
- 文本编辑工具:`edit_text` 与 `write_text`。
7
-
8
- 原生工具在 Windows 上存在两处缺陷:
9
-
10
- | 场景(文件为 UTF-8 **BOM + CRLF**) | 原生 `edit` | 原生 `write` | 本插件 |
11
- | ---------------------------------------- | ----------------------------- | ----------------------------------- | ----------------- |
12
- | 改一行 | CRLF 保住 / **BOM 丢失** | — | BOM + CRLF 都保住 |
13
- | 整篇覆盖 | — | **BOM 丢失 + CRLF 被拍成 LF** | BOM + CRLF 都保住 |
14
-
15
- 原因在于 `@deepseek-ai/dsh-fs-local` 完全没有 BOM 处理(Node 的 `TextDecoder` 默认吞掉前导
16
- BOM 字节),且 `writeText` 不按原文件风格还原行尾。
4
+ [DeepSeek Harness](https://github.com/deepseek-ai)(dsh)的模型工具插件,提供两个**字节保真**的文本编辑工具:`edit_text` 与 `write_text`。它替换原生 `write` / `edit`(由 `@deepseek-ai/dsh-fs-local` 实现)并修复以下缺陷:
17
5
 
18
- 在保真之外,本插件还提供:**统一 diff**(可用 dry-run 预览)、**写入前自动备份**、**编辑台账**、
19
- **`grep` / `lines` 锚点**(无需人工誊抄原文)、**歧义时拒绝写入**,以及**最接近候选**提示。
6
+ | 原生行为 | 成因 | 本插件 |
7
+ | ---- | ---- | ------ |
8
+ | 改一行或整篇覆盖丢失 UTF-8 BOM | 未实现 BOM 处理;Node 的 `TextDecoder` 默认吞掉前导 BOM 字节 | BOM 不变 |
9
+ | 整篇覆盖不还原行尾风格 | `writeText` 原样落盘 `content` | 行尾跟随文件 |
10
+ | `old_string` 差一个空格即报 **`FS_EDIT_NOT_FOUND`** | 原生 `edit` 只做精确匹配,没有回退 | 精确 → 宽松 → 最接近候选(歧义时拒绝写盘) |
20
11
 
21
- 两个工具的规范返回值、模型可见文本的构成与 UI 卡片的投影方式见「返回值」。
12
+ 此外提供 `grep` / `lines` 锚点。
22
13
 
23
14
  ## 实现与依赖
24
15
 
25
- 实现是**进程内 Node**(`lib/core.mjs`):只用 `node:` 内置模块,不启动任何子进程,无构建步骤、
26
- 无第三方依赖。
27
-
28
- | 依赖 | 说明 |
29
- | ---- | ------------------------------------------------------------------------------ |
30
- | Node | 插件唯一的依赖。不启动解释器、不引入外部运行时;每次调用均无进程启动开销。 |
16
+ 实现是**进程内 Node**(`lib/core.mjs`),不启动任何子进程,无构建步骤,无第三方依赖。
31
17
 
32
18
  ## 安装
33
19
 
34
- ### 方式一:preset(推荐,作用域最小)
20
+ ### 方式一:preset(作用域最小)
35
21
 
36
22
  仅选定该 preset 的会话可见这两个工具,其它项目与会话的工具表不受影响。**在本仓库根目录**执行:
37
23
 
@@ -45,8 +31,7 @@ node scripts/install-preset.mjs
45
31
 
46
32
  ### 方式二:安装到 profile(所有会话可用)
47
33
 
48
- `dsh plugin add` 支持多种来源,**以下来源均受支持**:本包为预构建的零依赖 ESM,无
49
- `prepare` / `build` 步骤,因此既无需用户授权构建,安装时也不会执行任何构建脚本。
34
+ 本包为预构建的零依赖 ESM,无 `prepare` / `build` 步骤:安装时不执行任何构建脚本,也不需要授权构建。下列来源均受支持:
50
35
 
51
36
  | 来源 | 命令 |
52
37
  | -------------- | --------------------------------------------------------------------------- |
@@ -61,147 +46,92 @@ node scripts/install-preset.mjs
61
46
  dsh --profile web --dump-config # 应当能看到 "# == @flotiarenor/dsh-tool-text-editor"
62
47
  ```
63
48
 
64
- `edit_text` / `write_text` 与原生工具不重名,插入宿主组合不会产生注册冲突。
49
+ `edit_text` / `write_text` 与原生工具不重名,插入宿主组合不会产生注册冲突。卸载:`dsh plugin --profile web remove @flotiarenor/dsh-tool-text-editor`。
50
+
51
+ | 机制 | 作用方式 |
52
+ | --- | --- |
53
+ | `apply()` 阶段挂在本行作用域层上的守卫 | 否决任何 agent 的首次原生调用,并在同一次调用中将其收窄 |
54
+ | 工具表与执行 | `agent.ctx.tools.restrict({ deny: ['write','edit'] })`——被点名的工具离开工具表,且无法调用 |
55
+ | 提示词 | `agent.ctx.systemPrompt.section({ name: 'tool:write', text: '' })` 遮蔽 `dsh-tool-fs` 注册的两段引导 |
56
+ | 退出组合与卸载 | `restrict` 与空段均注册在 **agent 自身的层**上,不随 preset 更换消失; |
57
+ | 宿主平面安装 | 挂在宿主平面(profile 层)的行没有作用域,守卫落入全局层,而该层的"收窄"会连带修改未挂本插件的 preset。
58
+
65
59
 
66
- 卸载:`dsh plugin --profile web remove @flotiarenor/dsh-tool-text-editor`。
60
+ ### 门禁行配置
67
61
 
68
- 两种方式可以并存:同名时 preset 层的注册会遮蔽宿主层的注册,二者定义相同、行为一致。
62
+ 不定义 Config schema:preset 行的 `config:` 字段原样透传。
63
+
64
+ | 键 | 默认 | 含义 |
65
+ | --- | --- | --- |
66
+ | `deny` | `['write','edit']` | 要收窄的名字 |
67
+ | `sections` | `['tool:write','tool:edit']` | 用空段遮蔽的引导段名,`[]` 关闭遮蔽 |
69
68
 
70
69
  ## 工具契约
71
70
 
72
71
  ### `edit_text` —— 局部替换
73
72
 
74
- | 参数 | 必填 | 说明 |
75
- | ------------- | ---- | ------------------------------------------------------------------------ |
76
- | `file_path` | ✅ | 目标文件;相对路径按会话工作区解析。 |
77
- | `new_text` | ✅ | 替换或插入的内容。 |
78
- | `old_text` | - | 字面量锚点(可直接复制 `read` 的输出)。 |
79
- | `grep` | - | 正则锚点:命中的行或行块作为锚点(含行尾换行符)。 |
80
- | `lines` | - | 行号锚点,如 `"263:270"` 或 `"120"`。 |
81
- | `mode` | - | `replace`(默认)/ `after` / `before` / `append` / `prepend`。 |
82
- | `count` | - | 要求恰好 N 处命中并全部替换(不符则拒绝写入)。 |
83
- | `nth` | - | 只替换第 k 处(1-based)。 |
84
- | `strict` | - | 禁用宽松匹配(只接受精确匹配)。 |
85
- | `diff` | - | 结果里 diff 正文的详细程度:`auto`(默认,改动小时才给)/ `full`(总给,仍封顶)/ `none`(不给)。 |
86
- | `dry_run` | - | 只输出 diff,不写入文件。 |
87
- | `note` | - | 一行说明,记入编辑台账。 |
88
-
89
- `old_text` / `grep` / `lines` **必须且只能提供一个**;`append` / `prepend` 不接受锚点,`after` / `before`
90
- 只能与 `grep` / `lines` 搭配。数量不符将拒绝写入。`count` 与 `nth` 互斥。
73
+ | 项 | 规则 |
74
+ | --- | --- |
75
+ | 必填 | `file_path` 与 `new_text` |
76
+ | 锚点,且恰好一个 | `old_text`(字面量,抄自 `read`)/ `grep`(正则;命中的行或行块,含行尾换行符)/ `lines`(如 `"263:270"`,同样含行尾换行符) |
77
+ | `mode` | `replace`(默认)/ `after` / `before` / `append` / `prepend` |
78
+ | `count` | 声明的命中数:`old_text` 为字面量出现次数(全部替换),`grep` 为正则命中处数,`lines` 为覆盖行数。与实际情况不符即拒绝写入 |
79
+ | 尾随换行 | 两种锚点都覆盖整行行块**并含行尾换行符**,因此 `new_text` 也应以换行结尾;否则替换会把下一行并入 |
80
+ | 匹配顺序 | 精确 → 宽松(忽略行尾空白、按行块相似度)→ 未命中时给出最接近的候选;命中多处且未声明 `count` 时拒绝写盘,宽松命中会在结果中附加一行 `[warn]` |
81
+ | 无"第 k 处" | `count` 是唯一的消歧旋钮,语义是**确认**而非**选择**。需要只改其中一处时,把锚点写到唯一——更长的 `old_text`,或改用 `lines` / `grep` |
82
+
83
+ `old_text` / `grep` / `lines` **必须且只能提供一个**;`append` / `prepend` 不接受锚点,`after` / `before` 只能与 `grep` / `lines` 搭配。**空的** `old_text` 会被拒绝:它没有指向任何内容(行块锚点请用 `lines` / `grep`)。
91
84
 
92
85
  ### `write_text` —— 整文件新建/覆盖
93
86
 
94
- `file_path` + `content`(另接受与 `edit_text` 相同的 `diff` / `dry_run` / `note`);目标不存在时自动
95
- 新建,覆盖前先备份。新建文件的行尾风格取自同目录的多数派(同扩展名优先),默认不写 BOM。
87
+ `file_path` + `content`;目标不存在时自动新建(含补齐缺失的父目录),新建文件的行尾风格跟随同目录(同扩展名优先),默认不写 BOM。**`content: ''` 配不存在的目标即创建零字节文件**(统计行为 `write +0/-0`);但向已为空的文件再写空内容仍按"没有产生任何变化"拒绝。
96
88
 
97
89
  ### 返回值
98
90
 
99
- 两个工具返回同一份规范值(`OUTPUT_SCHEMA`)。字段内容与去向如下:
100
-
101
- | 字段 | 内容 | 去向 |
102
- | --------------------- | ------------------------------------------------------------------------ | ---------------- |
103
- | `path` | 调用方给出的目标路径(原样回填) | — |
104
- | `ok` / `wrote` / `dryRun` | 执行结果标志 | — |
105
- | `brief` | 警告行与一行统计,如 `replace@60 +1/-1` | 模型上下文 |
106
- | `diff` | 只含 `@@` 块头与改动行的 unified diff(0 上下文行、无 `---` / `+++` 文件头),受 `maxDiffLines` 限制 | 模型上下文 |
107
- | `stdout` | 人读全文:路径头、含上下文行的完整 diff、备份文件名 | UI / 日志 / 排查 |
108
- | `stderr` | 失败原因(失败时非空) | 模型上下文 |
91
+ `render` 只读 `ok` / `brief` / `stderr`;`path` 为调用方保留,但不渲染。
109
92
 
110
- 模型可见文本由 `brief` 与 `diff` 组成,路径在起始行出现一次;`diff` 正文不含 `---` / `+++` 文件头,
111
- 因此路径不会在正文中再次出现。完整 diff 另经 `output.presentationMeta` 投影为
112
- `{ path, oldText, newText }` 列表,与原生 `edit` / `write` 的卡片词汇同形,由 `presentResult` 交给
113
- Web UI;该元数据随 `tool/result` 持久化,不进入模型上下文。
93
+ | 字段 | 内容 | 去向 |
94
+ | --- | --- | --- |
95
+ | `path` | 调用方给出的目标路径 | 仅规范值,**不渲染** |
96
+ | `ok` | 是否写入成功 | 模型上下文(`FAIL` 或 `WROTE`) |
97
+ | `brief` | 一行统计(如 `replace@17 +1/-1`)与必要的警告行 | 模型上下文 |
98
+ | `stderr` | 失败原因(失败时非空) | 模型上下文 |
114
99
 
115
- 失败时不返回 `brief` 与 `diff`:模型可见文本为 `FAIL` 加目标路径,其后是完整的失败原因;原因文本由
116
- 核心生成,其中通常再包含一次工作区相对路径(失败路径优先保证原因完整)。
100
+ 模型可见文本如下:
117
101
 
118
- `diff` 参数决定 `diff` 字段的详细程度:
119
-
120
- | 取值 | 行为 |
121
- | --------- | -------------------------------------------------------------------------------- |
122
- | `auto` | 默认。正文不超过 `maxDiffLines` 行时给出;超出时不给出正文,附一行省略提示 |
123
- | `full` | 始终给出正文;超过 `maxDiffLines` 行时截断,附一行截断提示 |
124
- | `none` | 不给出正文 |
125
-
126
- 正文固定使用 0 上下文行;`context` 配置只影响 `stdout` 与 UI 卡片。`maxDiffLines` 的作用是限制单次
127
- 调用回吐到模型上下文的字节数——工具结果按追加方式进入会话历史,不参与前缀缓存,因此不含上限时,
128
- 整文件重写会产生与输入内容等量级的回吐(实测放大率 ≈ 1.0x)。
102
+ ```
103
+ WROTE # 成功:一行统计 + 警告
104
+ replace@17 +1/-1
105
+ FAIL # 失败:完整原因(决定下一次调用)
106
+ <原因>
107
+ ```
129
108
 
130
109
  ## 已知限制
131
110
 
132
- 以下均为有意的设计取舍,而非缺陷;采用前请对照自身场景确认。
133
-
134
- - **写入不经由 `ctx.fs`。** 文件由本插件直接写入,因此不经过 fs 观察策略(先读后写、版本新鲜度校验)、
135
- 沙箱与 `sandbox_permissions` 审批升权,也不保留 Windows DACL。原子写由本插件自行实现
136
- (同目录临时文件 + fsync + rename);diff 卡片由本插件的 `presentationMeta` 提供,原生工具则取自
137
- `ctx.fs` 返回的 `before` / `after`。
138
- - **行号锚点不做内容校验。** `lines` 与 `before` / `after <行号>` 仅按行号定位:行号有误不会报错,
139
- 改动会落在非预期位置;定位需要可校验时,请改用 `old_text` 或 `grep`。
140
- - **同目标串行仅限本进程。** 进程内按目标路径排队,并配合原子写,故并行的工具调用不会相互覆盖;
141
- 但另一个 dsh 实例、编辑器或其它进程同时修改同一文件时,仍可能相互覆盖,本插件也不检测外部改动。
142
- - **仅处理 UTF-8 文本。** 含 NUL 字节的二进制文件与非法 UTF-8 文件一律拒绝;`.git/`、`.dsh/` 内部
143
- 以及工作区之外的路径一律拒绝写入。
111
+ | 限制 | 说明 |
112
+ | --- | --- |
113
+ | **写入不经由 `ctx.fs`** | 文件由本插件直接写入,因此不经过 fs 观察策略(先读后写、版本新鲜度校验)、沙箱、`sandbox_permissions` 审批升权,也不保留 Windows DACL;原子写由本插件自行实现(同目录临时文件 + fsync + rename)。这条路径上没有第二个强制点,插件因此把会话文件策略里**唯一禁止写入的那一档**镜像回来:`read-only` 会话下两个工具都在任何 I/O 之前拒写,原因中指明这是会话策略而非路径问题。`sandboxPolicy` 是可选消费(`ctx.get`),服务缺席或解析抛错时照旧写入,不会把写盘全部禁掉 |
114
+ | **只镜像 `read-only` 一档,且不做任何路径限制** | `workspace-write` 与 `danger-full-access` 下两个工具都能写**任何**路径:工作区之外、`.dsh/` 与 `.git/` 内部、以及工作区内指向外部的 junction / symlink 之后。本包**不是安全边界**:要限制可写范围,请用会话文件策略、沙箱与 `sandbox_permissions` |
115
+ | **行号锚点不做内容校验** | `lines` 与 `before` / `after <行号>` 仅按行号定位:行号有误不会报错,改动会落在非预期位置。定位需可校验时,请改用 `old_text` 或 `grep` |
116
+ | **同目标串行仅限本进程** | 进程内按目标路径排队,并配合原子写,因此并行的工具调用不会相互覆盖;但另一个 dsh 实例、编辑器或其它进程同时修改同一文件时,仍可能相互覆盖,本插件也不检测外部改动 |
117
+ | **仅处理 UTF-8 文本** | 含 NUL 字节的二进制文件与非法 UTF-8 文件一律拒绝;被操作系统标记为只读的文件同样拒绝(原子 rename 会以 `EPERM` 失败) |
144
118
 
145
119
  ## 配置
146
120
 
147
121
  本插件不定义 Config schema:preset 行的 `config:` 字段原样透传。
148
122
 
149
- | 键 | 默认 | 含义 |
150
- | ---------------- | ----------------- | ------------------------------------------------ |
151
- | `backup` | `true` | 写入前把原内容复制到 `artifactsDir/backups` |
152
- | `ledger` | `true` | 往 `artifactsDir/edits.log` 追加一条 JSONL 记录 |
153
- | `artifactsDir` | `<工作区>/.dsh` | 备份与台账所在目录 |
154
- | `newFileBom` | `false` | 新建文件时是否写 UTF-8 BOM |
155
- | `context` | `3` | `stdout` 与 UI 卡片中 diff 的上下文行数(模型可见正文固定 0 行) |
156
- | `diff` | `'auto'` | `diff` 字段的默认策略(`auto` / `full` / `none`);逐调用的 `diff` 参数优先 |
157
- | `maxDiffLines` | `30` | `diff` 字段的行数上限:`auto` 超出时不给出正文,`full` 超出时截断 |
158
- | `root` | `process.cwd()` | 无 agent 会话时的回退工作区 |
123
+ | 键 | 默认 | 含义 |
124
+ | --- | --- | --- |
125
+ | `newFileBom` | `false` | 新建文件时是否写 UTF-8 BOM |
126
+ | `guidance` | `'full'` | 三档:`full`(指名原生工具)/ `short`(门禁屏蔽原生工具时用)/ `false`(整段不注册) |
127
+ | `root` | `process.cwd()` | 无 agent 会话时的回退工作区 |
159
128
 
160
129
  环境变量 `DSH_TEXT_EDITOR_EOL`(`lf` \| `crlf`)可覆盖**新建文件**的行尾推断。
161
130
 
162
- ## 自测与门禁
163
-
164
- ```powershell
165
- # 在本仓库根目录执行
166
- node tools/selftest.mjs # Windows + Node 24 参考结果 92/92
167
- node tools/check-license.mjs # 许可证 / 依赖 / 纯 Node 门禁
168
- node tools/gen-schema.mjs # 内嵌 schema 是否仍与作者 DSL 一致
169
- ```
170
-
171
- `tools/selftest.mjs` 覆盖:BOM 与行尾保真、`dry_run`、四种锚点、`count`、歧义时拒绝写入、用法错误、
172
- 二进制与非法 UTF-8、路径护栏(`.dsh/`、工作区之外)、行尾多数派推断、跨多个 hunk、末尾换行差异、
173
- 并发写入不产生半截文件;**并含一层插件层断言**:以模拟 ctx 驱动 `apply()`,验证工具注册、引导段
174
- 身份、每个返回值均满足 `OUTPUT_SCHEMA`、`render()` 输出,以及 config 透传(`root` / `backup` /
175
- `ledger` / `newFileBom`);**以及一层返回值约束断言**:整文件重写不回显内容、小改动仍给出改动行、
176
- `diff` 三种取值的行为边界、路径仅出现一次、完整 diff 只经 `presentationMeta` 投影。
177
-
178
- `tools/gen-schema.mjs` 需要一份装有 `@deepseek-ai/dsh-tools` 的 dsh:它会在 dsh profile 的
179
- `node_modules` 与 npm 全局目录中自动查找,也可用 `DSH_TOOLS_ENTRY` 显式指定;找不到入口时退出码为 2。
180
-
181
- ## 目录结构
182
-
183
- ```
184
- lib/core.mjs # 编辑核心:BOM/行尾、锚点、匹配、diff、备份、台账、原子写、同目标串行
185
- lib/editor.mjs # 插件本体:schema、参数校验、工具注册(零依赖 ESM,无构建)
186
- preset/preset.yml # preset 的名字/描述(dsh 列表里显示的内容)
187
- scripts/install-preset.mjs # 从本机 dsh 派生用户 preset
188
- cordis.patch.yml # 宿主平面安装用的 bundle patch
189
- tools/selftest.mjs # 端到端自测(核心 + 插件层)
190
- tools/check-license.mjs # 许可证 / 依赖 / 纯 Node 卫生门禁
191
- tools/gen-schema.mjs # 内嵌 schema 的权威来源与校验器
192
- ```
193
-
194
- 备份与台账采用固定的命名与字段:每次编辑在 `.dsh/backups/` 下留存一个文件,命名为
195
- `<绝对路径扁平化>@<时间戳>`;`.dsh/edits.log` 每行一个 JSON 对象(`time`、`id`、`tool`、`file`、
196
- `abspath`、`action`、`kinds`、`line_start`、`line_end`、`added`、`removed`、`bom`、`eol`、
197
- `backup`、`summary`)。
198
-
199
131
  ## License
200
132
 
201
- **Apache-2.0**,见 [LICENSE](LICENSE)。Copyright 2026 Flotiarenor。本包**零运行时依赖**,因此不承担
202
- 任何第三方许可证义务。
133
+ **Apache-2.0**,见 [LICENSE](LICENSE)。Copyright 2026 Flotiarenor。本包**零运行时依赖**,因此不承担任何第三方许可证义务。
203
134
 
204
- - `lib/editor.mjs` 内嵌的 JSON Schema 是 `@deepseek-ai/dsh-tools`(MIT,Copyright (c) 2026 DeepSeek)
205
- 转换器的**生成产物**(由 `tools/gen-schema.mjs` 离线生成)。
135
+ - `lib/editor.mjs` 内嵌的 JSON Schema 是 `@deepseek-ai/dsh-tools`(MIT,Copyright (c) 2026 DeepSeek)转换器的**生成产物**(由 `tools/gen-schema.mjs` 离线生成)。
206
136
  - preset 组合**不在本包内**:`scripts/install-preset.mjs` 在安装时读取使用者所装 dsh 自带的组合。
207
137
  - 源文件均带 `SPDX-License-Identifier` 头,许可证**逐文件机器可读**。