|
| 1 | +# 祖龙 IDE 写文件工具新增 edits 模式设计方案 |
| 2 | + |
| 3 | +> 状态:设计文档,暂不改业务代码。 |
| 4 | +> 对齐 TSD 1.7 §23(IDE 工具体系)。 |
| 5 | +
|
| 6 | +--- |
| 7 | + |
| 8 | +## 1. 背景 |
| 9 | + |
| 10 | +### 1.1 问题 |
| 11 | + |
| 12 | +当前 `ide_write_file` 只有 `overwrite` 和 `append` 两种模式,LLM 写长文件时面临: |
| 13 | + |
| 14 | +1. **JSON 截断**:文件内容在 tool_call JSON 中,受 `max_tokens` 限制,长内容 JSON 会被截断导致解析失败 |
| 15 | +2. **内容冗余**:LLM 做小修改(修 bug、加函数)也必须输出完整文件内容,浪费 token 和网络带宽 |
| 16 | +3. **用户体验差**:用户看不到增量的代码变更,只能看到"文件被覆写/追加" |
| 17 | + |
| 18 | +### 1.2 Cline 现有能力(已审计) |
| 19 | + |
| 20 | +Cline 插件(`VscodeExecutionBridge.ts`)已有完整的编辑器操作能力: |
| 21 | + |
| 22 | +| 操作 | 方法 | 实现方式 | |
| 23 | +|---|---|---| |
| 24 | +| `write_to_file` | `writeFile()` (L222-246) | `fs.writeFile` 完整写入,走 `confirmFileChange` 审批 | |
| 25 | +| `replace_in_file` | `replaceInFile()` (L267-287) | SEARCH/REPLACE 格式 diff,`applySearchReplaceDiff` | |
| 26 | +| `create_directory` | `createDirectory()` (L248-261) | `fs.mkdir` | |
| 27 | +| `delete_file` | `deleteFile()` (L314+) | `fs.unlink` | |
| 28 | + |
| 29 | +`replace_in_file` 已经提供了 diff 编辑能力。**但 Python 工具层没有暴露这个功能。** |
| 30 | + |
| 31 | +### 1.3 当前 Python→Cline 通信路径 |
| 32 | + |
| 33 | +``` |
| 34 | +ide_write_file.execute() (Python) |
| 35 | + → _run_async_request("ide:execute_tool", payload) (ide_bridge_tools.py:822) |
| 36 | + → ide_server.request_ide_action("ide_execute_tool", payload) (ide_server.py:843) |
| 37 | + → WebSocket → Cline executeTool(toolName, args) |
| 38 | + → switch(toolName) { case "write_to_file": ... } (VscodeExecutionBridge.ts:143-148) |
| 39 | +``` |
| 40 | + |
| 41 | +--- |
| 42 | + |
| 43 | +## 2. 方案决策 |
| 44 | + |
| 45 | +### 2.1 是否直接改造 ide_write_file? |
| 46 | +**否,使用"扩展 mode 参数"方式,而非替换。** |
| 47 | + |
| 48 | +`ide_write_file` 的 `overwrite` 和 `append` 模式仍然需要,不可删除: |
| 49 | + |
| 50 | +| 场景 | 必需模式 | 原因 | |
| 51 | +|---|---|---| |
| 52 | +| 创建新文件 | overwrite | 从头建文件,无法 diff | |
| 53 | +| 追加内容 | append | 增量追加,适合日志/报告 | |
| 54 | +| 创建目录 | create_directory | 非文件操作 | |
| 55 | +| 修改现有文件 | edit(新增) | 精准编辑,不传输完整内容 | |
| 56 | + |
| 57 | +### 2.2 edits vs diff 模式对比 |
| 58 | + |
| 59 | +| 维度 | SEARCH/REPLACE diff | Position-based edits | |
| 60 | +|---|---|---| |
| 61 | +| 格式 | `<<<<<<< SEARCH\n原文\n=======\n替换\n>>>>>>> REPLACE` | `[{op, line/range, text}]` | |
| 62 | +| LLM 生成难度 | 中等(需精确匹配原文字符) | 简单(只需行号+内容) | |
| 63 | +| 可靠性 | 差(空白/编码不一致→匹配失败) | 好(行号定位,容错性好) | |
| 64 | +| Cline 现成实现 | ✅ `replaceInFile` | ❌ 需新增 | |
| 65 | +| 实时编辑器更新 | ❌ 全文写入后 reload | ✅ WorkspaceEdit API | |
| 66 | +| 多块编辑 | 支持(可串多块 SEARCH/REPLACE) | 支持(操作列表) | |
| 67 | + |
| 68 | +**推荐**:先复用 Cline 现有的 `replace_in_file`(最小投入),同时设计 `edit` 模式协议(中长期优化)。 |
| 69 | + |
| 70 | +### 2.3 方案 |
| 71 | + |
| 72 | +**Phase 1(立即可做)**:在 Python 工具层新增 `ide_replace_file` 工具,透传 Cline 的 `replace_in_file` 能力。**零 Cline 端改动。** |
| 73 | + |
| 74 | +**Phase 2(后续优化)**:新增 `ide_write_file(mode="edit")`,支持基于位置的操作列表,需要在 Cline 端新增 handler。 |
| 75 | + |
| 76 | +--- |
| 77 | + |
| 78 | +## 3. Phase 1 设计:ide_replace_file(复用 Cline replace_in_file) |
| 79 | + |
| 80 | +### 3.1 新增工具 |
| 81 | + |
| 82 | +```python |
| 83 | +# zulong/tools/ide_bridge_tools.py 新增 |
| 84 | + |
| 85 | +class IdeReplaceFileTool(BaseTool): |
| 86 | + """通过 SEARCH/REPLACE diff 替换文件内容(复用 Cline replace_in_file)。""" |
| 87 | + |
| 88 | + def __init__(self): |
| 89 | + super().__init__(name="ide_replace_file", category=ToolCategory.CODE) |
| 90 | + self.description = ( |
| 91 | + "使用 SEARCH/REPLACE 格式差异替换文件中的指定内容。" |
| 92 | + "适合对已有文件做精准修改(修 bug、加函数、改配置),无需传输完整文件内容。" |
| 93 | + "diff 格式:第一块以 <<<<<<< SEARCH 开头,然后是原标题," |
| 94 | + "======= 分隔,然后是替换内容,>>>>>>> REPLACE 结尾。" |
| 95 | + "可串联多块 diff。" |
| 96 | + "如果修改内容较多建议直接用 ide_write_file(mode='overwrite') 重写整个文件。" |
| 97 | + ) |
| 98 | +``` |
| 99 | + |
| 100 | +### 3.2 WebSocket 协议(无改动) |
| 101 | + |
| 102 | +复用现有 `ide:execute_tool` 协议,Cline 已有 handler: |
| 103 | + |
| 104 | +```json |
| 105 | +// Python→Cline WebSocket 消息(现有协议,无需改动) |
| 106 | +{ |
| 107 | + "type": "ide_execute_tool", |
| 108 | + "tool_name": "replace_in_file", |
| 109 | + "arguments": { |
| 110 | + "path": "/workspace/src/main.py", |
| 111 | + "diff": "<<<<<<< SEARCH\ndef old_func():\n pass\n=======\ndef old_func():\n return 42\n>>>>>>> REPLACE" |
| 112 | + } |
| 113 | +} |
| 114 | +``` |
| 115 | + |
| 116 | +### 3.3 与 ide_write_file 的联动 |
| 117 | + |
| 118 | +`ide_replace_file` 适合 LLM 自主选择使用。当 LLM 调用 `ide_write_file` 且文件已存在时,可在 prompt 中引导优先使用 `ide_replace_file` 做轻量修改。 |
| 119 | + |
| 120 | +--- |
| 121 | + |
| 122 | +## 4. Phase 2 设计:ide_write_file edits 模式 |
| 123 | + |
| 124 | +### 4.1 新增 mode 值 |
| 125 | + |
| 126 | +`ide_write_file` 新增 `mode="edit"`,接受 `edits` 参数: |
| 127 | + |
| 128 | +```python |
| 129 | +# ide_write_file 参数扩展 |
| 130 | +{ |
| 131 | + "mode": "edit", # 新增值 |
| 132 | + "file_path": "/workspace/src/main.py", |
| 133 | + "edits": [ # 新增参数 |
| 134 | + {"op": "insert", "line": 5, "text": "import os\n"}, |
| 135 | + {"op": "delete", "line": 3, "count": 2}, |
| 136 | + {"op": "replace", "start_line": 10, "end_line": 12, "text": "def new_func():\n pass\n"}, |
| 137 | + {"op": "append", "text": "\n# New section\n"} |
| 138 | + ] |
| 139 | +} |
| 140 | +``` |
| 141 | + |
| 142 | +### 4.2 编辑操作类型 |
| 143 | + |
| 144 | +| op | 必需参数 | 语义 | |
| 145 | +|---|---|---| |
| 146 | +| `insert` | `line`, `text` | 在第 `line` 行前插入文本 | |
| 147 | +| `delete` | `line`, `count` | 从第 `line` 行开始删除 `count` 行 | |
| 148 | +| `replace` | `start_line`, `end_line`, `text` | 替换第 `start_line` 到 `end_line`(含)的行为 `text` | |
| 149 | +| `append` | `text` | 追加到文件末尾 | |
| 150 | + |
| 151 | +### 4.3 WebSocket 协议 |
| 152 | + |
| 153 | +```json |
| 154 | +// Python→Cline 新消息类型 |
| 155 | +{ |
| 156 | + "type": "ide_execute_tool", |
| 157 | + "tool_name": "apply_edits", |
| 158 | + "arguments": { |
| 159 | + "path": "/workspace/src/main.py", |
| 160 | + "edits": [ |
| 161 | + {"op": "insert", "line": 5, "text": "import os\n"}, |
| 162 | + {"op": "delete", "line": 3, "count": 2} |
| 163 | + ] |
| 164 | + } |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +### 4.4 Cline 端实现(伪代码) |
| 169 | + |
| 170 | +```typescript |
| 171 | +// VscodeExecutionBridge.ts 新增 |
| 172 | +case "apply_edits": |
| 173 | + return this.applyEdits(args) |
| 174 | + |
| 175 | +private async applyEdits(args: Record<string, any>): Promise<string> { |
| 176 | + const filePath = this.resolvePath(args.path) |
| 177 | + this.ensureInsideWorkspace(filePath) |
| 178 | + const original = await fs.readFile(filePath, "utf-8") |
| 179 | + const lines = original.split("\n") |
| 180 | + const edits: EditOp[] = args.edits || [] |
| 181 | + |
| 182 | + for (const edit of edits) { |
| 183 | + switch (edit.op) { |
| 184 | + case "insert": lines.splice(edit.line - 1, 0, ...edit.text.split("\n")); break |
| 185 | + case "delete": lines.splice(edit.line - 1, edit.count); break |
| 186 | + case "replace": lines.splice(edit.start_line - 1, edit.end_line - edit.start_line + 1, ...edit.text.split("\n")); break |
| 187 | + case "append": lines.push(...edit.text.split("\n")); break |
| 188 | + } |
| 189 | + } |
| 190 | + const next = lines.join("\n") |
| 191 | + |
| 192 | + const approved = await this.confirmFileChange({ |
| 193 | + filePath, original, next, |
| 194 | + operation: "modify", |
| 195 | + summary: `应用 ${edits.length} 个编辑操作`, |
| 196 | + }) |
| 197 | + if (!approved) return `用户未应用编辑: ${filePath}` |
| 198 | + |
| 199 | + await fs.writeFile(filePath, next, "utf-8") |
| 200 | + this.sendFileChanged(filePath, "edited") |
| 201 | + return `已应用 ${edits.length} 个编辑操作: ${filePath}` |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | +### 4.5 为什么 edits 优于 diff |
| 206 | + |
| 207 | +| 场景 | diff(SEARCH/REPLACE) | edits(行操作) | |
| 208 | +|---|---|---| |
| 209 | +| 换空白字符 | ❌ 匹配失败 | ✅ 不受影响 | |
| 210 | +| 文件已被人修改 | ❌ search 文本不匹配 | ✅ 行号可能偏移但容错 | |
| 211 | +| LLM 生成难度 | 高(需输出原文字符) | 低(只需行号 + 新内容) | |
| 212 | +| 多块编辑 | 支持 | 支持 | |
| 213 | +| 传输量 | 含原文字符,较大 | 只含新内容,小 | |
| 214 | + |
| 215 | +--- |
| 216 | + |
| 217 | +## 5. Phase 2 备选方案:直接用 VS Code WorkspaceEdit API |
| 218 | + |
| 219 | +Cline 插件可以直接调 `vscode.WorkspaceEdit`,在编辑器里实时应用修改,不需要 `fs.readFile`+`fs.writeFile`(当前 `writeFile` 和 `replaceInFile` 的实现方式)。 |
| 220 | + |
| 221 | +```typescript |
| 222 | +// 更优实现:直接用 WorkspaceEdit(免 fs 读写) |
| 223 | +const uri = vscode.Uri.file(filePath) |
| 224 | +const edit = new vscode.WorkspaceEdit() |
| 225 | +for (const op of edits) { |
| 226 | + const pos = new vscode.Position(op.line - 1, 0) |
| 227 | + switch (op.op) { |
| 228 | + case "insert": edit.insert(uri, pos, op.text); break |
| 229 | + case "delete": edit.delete(uri, new vscode.Range(pos, pos.translate(op.count, 0))); break |
| 230 | + case "replace": edit.replace(uri, new vscode.Range( |
| 231 | + new vscode.Position(op.start_line - 1, 0), |
| 232 | + new vscode.Position(op.end_line - 1, Number.MAX_SAFE_INTEGER) |
| 233 | + ), op.text); break |
| 234 | + } |
| 235 | +} |
| 236 | +await vscode.workspace.applyEdit(edit) |
| 237 | +// → 编辑器实时更新,合并到 undo 栈,用户看得见光标变化 |
| 238 | +``` |
| 239 | + |
| 240 | +**优势**: |
| 241 | +- 编辑器内实时更新,光标跟随 |
| 242 | +- 修改进 VS Code 的 undo stack(Ctrl+Z 可撤) |
| 243 | +- 不触发文件系统 IO,更快 |
| 244 | + |
| 245 | +**当前不推荐推进**:因为 Cline 的 `confirmFileChange` 审批流程依赖"原内容 vs 新内容"的 diff 对比,`WorkspaceEdit` 直接操作编辑器缓冲区不走这个审批流程,需要重构审批机制。**后续可做,当前先走 `fs.readFile`+`fs.writeFile` 方式。** |
| 246 | + |
| 247 | +--- |
| 248 | + |
| 249 | +## 6. 实施路径 |
| 250 | + |
| 251 | +| 阶段 | 内容 | 改动文件 | 工作量 | |
| 252 | +|---|---|---|---| |
| 253 | +| **Phase 1** | 新增 `ide_replace_file` 工具(复用 Cline `replace_in_file`) | `ide_bridge_tools.py`(新增 1 个类) | ~50 行 Python | |
| 254 | +| **Phase 1** | 工具注册 + LLM prompt 提示 | `tool_registry.py`(注册新工具) | ~5 行 | |
| 255 | +| **Phase 2** | `ide_write_file` 新增 `mode="edit"` + `edits` 参数 | `ide_bridge_tools.py`(扩展 IdeWriteFileTool.execute) | ~80 行 Python | |
| 256 | +| **Phase 2** | Cline 新增 `apply_edits` handler | `VscodeExecutionBridge.ts`(新增 case) | ~60 行 TS | |
| 257 | +| Phase 2(可选) | 切到 `WorkspaceEdit` API | `VscodeExecutionBridge.ts` | 需重构审批流 | |
| 258 | + |
| 259 | +--- |
| 260 | + |
| 261 | +## 7. LLM Prompt 引导 |
| 262 | + |
| 263 | +在 system prompt 中新增: |
| 264 | + |
| 265 | +``` |
| 266 | +## 文件编辑策略 |
| 267 | +
|
| 268 | +- 创建新文件:使用 ide_write_file(mode="overwrite", content="完整内容") |
| 269 | +- 追加内容:使用 ide_write_file(mode="append", content="追加内容") |
| 270 | +- 修改已有文件(轻量):优先使用 ide_replace_file(diff="SEARCH/REPLACE") |
| 271 | +- 修改已有文件(多处修改):使用 ide_write_file(mode="edit", edits=[...]) |
| 272 | +- 完全重写文件:使用 ide_write_file(mode="overwrite") |
| 273 | +``` |
| 274 | + |
| 275 | +--- |
| 276 | + |
| 277 | +## 8. 与现有 ide_write_file 的兼容性 |
| 278 | + |
| 279 | +- `overwrite` 和 `append` 模式行为不变,向后兼容 |
| 280 | +- `mode="edit"` 是新增值,老代码不受影响 |
| 281 | +- `ide_replace_file` 是完全独立的新工具 |
| 282 | +- 所有现有审批流程(`confirmFileChange`、`require_folder_access_authorization`、路径白名单)保持不变 |
| 283 | + |
| 284 | +--- |
| 285 | + |
| 286 | +## 9. 后续工作 |
| 287 | + |
| 288 | +- Phase 1 实施后,`_try_repair_truncated_json`(之前加的截断修复)可以降级为兜底——因为 LLM 用 `ide_replace_file` 时不再输出长 content,JSON 截断概率大幅降低 |
| 289 | +- 长期考虑在 Cline 端用 `vscode.WorkspaceEdit` 实现编辑,给用户实时编辑体验 |
| 290 | +- edits 模式稳定后,考虑自动降级:当 LLM 的 `ide_write_file` 内容超 2000 字符时,工具端自动拒绝并建议切换 edits 模式 |
0 commit comments