Files
DevFlow/docs/02-架构设计/专项设计/patch_file工具设计-2026-06-15.md
绝尘 8d18918e39 更新: 架构设计文档状态同步代码核验结果
经代码核验发现 9 份设计文档的状态标注严重滞后(标'待实施'但
实际已完整落地),本次批量同步:

已落地(核验确认):
- 局部编辑工具:三层防御+三模式完整,仅文本不支持二进制
- 密钥迁移健壮性:空 key 不覆盖+即时迁移补密钥+阻断保存
- AST 符号解析:符号读取工具已注册+基线测试守护
- 查询能力补全:任务/项目/灵感均多维动态查询
- 条件表达式引擎:手写求值器+JSON Path+执行器集成+前端入口
- 工作流脚本边界:命令白名单/黑名单+危险关键词告警
- 消息拆分存储:消息表+全量迁移+读写全部切换
- 消息级溯源:消息 ID+四场景溯源+切读全部完成

部分落地:
- 全局事件总线:基建+20 余个发射点就位,消费者未接(空转)

归档不实施:
- 规格契约自检:核心价值已被求助协议+自审闸门覆盖,过度设计
2026-06-29 00:16:39 +08:00

361 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Patch File 工具设计
> 创建: 2026-06-15 | 状态: ✅ 已落地(2026-06-28 核验) | 优先级: P0
> 关联 todo: F-260615-06 [P0]
---
## 一、问题定义
### 1.1 现状缺口
DevFlow AI agent 有 `write_file`(全量覆盖写入)但**无局部编辑能力**。AI 需要修改文件中某几行时只能:
```
read_file → AI 在 context 中拼出完整新内容 → write_file 全量覆盖
```
**问题链**
1. **Token 浪费**:大文件(几百行)只改 3 行却要重发全部内容
2. **事故风险**write_file 全量覆盖已出事故PROGRESS.md 762 行 → 248 字节FR-S7 记录)
3. **无审计粒度**:无法知道「改了哪里」,只有「整个文件被替换了」
4. **并发不安全**join_all 并行场景下多工具操作同一文件无保护
### 1.2 目标
提供 `patch_file` 工具,让 AI 能做**精确的局部文本替换**,形成完整的文件操作闭环:
```
read_file(读) → search_in_file(定位) → patch_file(改) → run_command(验证)
```
---
## 二、API 设计
### 2.1 请求结构
```rust
/// 局部文件更新请求
struct PatchFileRequest {
/// 目标文件路径(必填,走 validate_path 校验 + 黑名单)
path: String,
/// 文件指纹(可选):用于检测外部修改
/// 格式: "{unix_timestamp}_{size}" 如 "1718400000_12345"
/// 由 read_file 返回的 file_hash 字段携带
expected_hash: Option<String>,
/// 有序补丁列表(从文件末尾往前执行,避免行号偏移)
patches: Vec<Patch>,
}
/// 单个补丁
struct Patch {
/// 必填:要替换的旧文本(精确匹配 = 乐观锁)
old_text: String,
/// 必填:替换后的新文本
new_text: String,
/// 可选:行号辅助定位(快速跳转 + 去歧增强)
/// 有值时优先跳到该行检查 old_text不匹配则降级全文扫描
line: Option<u32>,
/// 可选old_text 之前的上下文锚(去歧——多匹配时精确锁定)
before_text: Option<String>,
/// 可选old_text 之后的上下文锚(去歧)
after_text: Option<String>,
}
```
### 2.2 响应结构
```rust
struct PatchResult {
success: bool,
patches_applied: usize, // 成功替换的 patch 数
total_matches: usize, // 每个 patch 的总命中数(含未替换的)
lines_changed: i32, // 总行数变化(正=增加 负=减少)
warnings: Vec<String>, // ["匹配到 3 处,仅替换第 1 处"]
file_hash: String, // 操作后的新指纹(下次操作用)
}
```
### 2.3 read_file 扩展(返回指纹)
现有 read_file 返回值新增字段:
```rust
struct ReadFileResult {
path: String,
content: String,
size: u64,
lines: u32,
// 新增:
modified: String, // ISO8601 或 unix timestamp
file_hash: String, // mtime+size 指纹,如 "1718400000_12345"
}
```
### 2.4 LLM 描述tool_registry 注册用)
```
"局部更新文件内容。用于精确修改文件的特定部分(而非全量覆盖)。
每个补丁指定 old_text要替换的原文和 new_text新内容
可选 line 辅助定位、before/after_text 上下文锚定消除歧义。
属 Medium 风险操作(修改已有文件),需人工审批。
注意old_text 必须与文件内容完全匹配(含空格/缩进);若文件已被外部修改,
请先重新 read_file 获取最新内容和 file_hash。"
```
---
## 三、核心决策记录
### 决策 1定位方式 — old_text 精确匹配为主line 为辅
| 方案 | 示例 | 优点 | 缺点 |
|------|------|------|------|
| **A: old_text ✅ 选定** | 匹配 "fn main() {" 替换 | =隐式乐观锁AI零认知负担复制即用内容变了自动冲突报错 | 改动大时 old_string 长 |
| B: line 行号 | 替换第 42 行 | 短小精悍 | 文件改了行号偏移AI需先search定位多一轮IPC |
| C: 正则 regex | 匹配 `/return Err\(.*\)/` | 表达力强 | AI生成regex易出错转义复杂 |
**折中**old_text **必填**主定位line **可选**(辅助快速跳转+去歧before/after_text **可选**(多匹配去歧)。
**理由**
- AI 做 edit 时天然持有「要改哪段」上下文,复制即用(最小认知路径)
- old_text 天然防并发冲突CAS 语义Compare-And-Swap
- line 不单独使用(无内容校验=盲替换,并发不安全)
### 决策 2多匹配处理 — 替换第 1 处 + warning
```
文件中有 N(N>1) 处相同 old_text:
→ 仅替换第 1 处
→ 返回 warning: "⚠️ 匹配到 N 处,仅替换第 1 处"
→ AI 收到 warning 后可加 before/after_text 缩小范围重试
```
**不选**「全部替换」(太危险,可能批量错改)或「拒绝执行」(太严格,第 1 处往往就是目标)。
### 决策 3并发安全 — 文件级 Mutex不用队列
#### 为什么不用队列
| 维度 | 通用文件锁队列 | DevFlow 实际需要 |
|------|-------------|-----------------|
| 范围 | 全局、所有会话、所有文件 | 仅 join_all 并发窗口 + 远期多会话 |
| 粒度 | 每文件独立 FIFO 队列 | 文件级 Mutex 就够 |
| 复杂度 | 高(调度/超时/死锁检测) | **极低(~15行** |
| 场景 | 多用户 / 分布式 | 单进程单用户桌面应用 |
#### 当前真实并发源
```
唯一并行点: audit.rs:338 join_all — Low 风险工具并行执行
典型场景: AI 同时 list_projects + read_file + (未来) patch_file 同一文件
```
#### 实现
```rust
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Mutex;
use once_cell::sync::Lazy;
/// 全局文件锁表:每个路径一把互斥锁
static FILE_LOCKS: Lazy<Mutex<HashMap<PathBuf, ()>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
// handler 内使用:
let abs_path = validated_path.canonicalize()?;
let _guard = FILE_LOCKS
.lock()
.entry(abs_path)
.or_insert_with(|| ());
// guard drop 时自动释放
```
**效果**同一文件读写串行化不同文件仍并行。Mutex 释放后后续操作继续。
### 决策 4外部脏写防御 — expected_hash 指纹校验
#### 指纹选型
| 方案 | 精度 | 开销 | 适用 |
|------|------|------|------|
| **mtime + size ✅ 选定** | 秒级 | ~μs一次 metadata 调用) | 本地桌面应用 |
| blake3/sha256 | 内容级 | 大文件 ms 级 | 需要密码学强度时 |
| inode + mtime | Unix 语义 | ~μs | Windows inode 不同 |
**mtime + size**DevFlow 是本地桌面应用,「外部修改」= 用户切 VS Code 改了几行再回来,时间差 >1s。实现最简单。
#### 流程
```
read_file(path) → { content, file_hash: "1718400000_23456" }
AI 基于内容决策 ↓
patch_file({ path, expected_hash: "1718400000_23456", ... })
后端:
① current_meta = metadata(path)
② current_hash = format!("{}_{}", modified.timestamp(), size)
③ current_hash != expected_hash?
→ Err("⚠️ 文件已被外部修改(hash 不匹配),请重新读取")
含 current_hash 让前端可选自动重读
④ hash 通过 → 执行 patchL2 old_text 校验 + L3 .bak
```
### 决策 5截断策略 — 软删除标记(非真删)
关联 UX-2025-09 编辑消息功能。patch_file 本身不涉及消息截断,但设计原则一致:
```
messages 表: status 列
active — 正常显示
truncated — 被编辑截断(前端不展示,后端不进 context
保留历史可追溯,与 WF-A soft_delete 模式一致
```
---
## 四、三层防御架构
```
┌─────────────────────────────────────────────┐
│ L1: 文件级 Mutex │
│ 防时机冲突:同文件读写自动串行化 │
│ 实现: HashMap<PathBuf, Mutex<()>> (~15行) │
│ │
│ ┌───────────────────────────────────────┐ │
│ │ L2: old_text 精确匹配 │ │
│ │ 防内容错配:= 乐观锁(CAS) │ │
│ │ 内容变了 → 匹配不上 → 报错 │ │
│ │ 实现: 内容扫描 (~20行) │ │
│ │ │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ L3: expected_hash 指纹校验 │ │ │
│ │ │ 防版本漂移: 外部修改检测 │ │ │
│ │ │ 实现: metadata() (~10行) │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └───────────────────────────────────────┘ │
│ │
│ 底层兜底: FR-S7 .bak 备份(误操作可恢复) │
└─────────────────────────────────────────────┘
```
各层职责独立、互补:
| 层 | 管 | 防什么 | 失效后果 |
|----|-----|--------|---------|
| L1 Mutex | 时机 | 两操作同时碰同一文件 | 数据丢失/半写 |
| L2 old_text | 内容 | 操作基于过时内容 | 错改别处 |
| L3 hash | 版本 | read→patch之间文件被外部改 | 基于错误版本操作 |
| .bak | 恢复 | 以上全失效时的最后防线 | 可回滚 |
---
## 五、查找策略line + old_text 组合)
```
有 line 参数?
├─ YES → 跳到该行,检查周围是否包含 old_text
│ ├─ 匹配 → 替换(快速路径 ✅)
│ └─ 不匹配(行已偏移) → 降级: 全文扫描 old_text
└─ NO → 全文扫描 old_text
全文扫描结果:
├─ 0 处匹配 → Err("未找到目标文本,文件可能已被修改")
├─ 1 处匹配 → 替换 ✅
└─ N 处匹配(N>1) → 替换第 1 处 + warning("匹配到 N 处...")
```
---
## 六、边界情况处理
| 边界情况 | 行为 | 理由 |
|---------|------|------|
| 文件不存在 | Err("文件不存在") | 安全第一 |
| 文件 >1MB | warn + 继续或拒绝(复用 FR-S2 上限) | 防性能问题 |
| 二进制文件(含 \0 | Err("不支持二进制文件") | 文本操作不适用 |
| old_text 为空串 | Err("old_text 不能为空") | 防全文件匹配 |
| new_text == old_text | success + warning("无实际更改") | 不浪费 I/O |
| patches 为空 | Err("patches 不能为空") | 无意义调用 |
| 单次 patch 文件膨胀 >900% | warn复用 FR-S7 逻辑) | 异常检测 |
| 目标路径是 .bak/.tmp | is_noise_file 过滤拒绝CR-03 | 不改临时文件 |
| 路径含 ".." | validate_path 黑名单拦截 | 路径遍历防护 |
| RiskLevel | **Medium**(修改已有文件) | 比 write_file 同级(都是改文件) |
---
## 七、性能分析
| 操作 | 开销 | 对比基准 |
|------|------|---------|
| Mutex 获取/释放 | ~100ns非竞争/ μs 级(等待) | 文件 I/O 是 ms 级,可忽略 |
| 全文扫描 old_text | O(n), n=行数(<1MB) | <1ms |
| hash 计算 | metadata() 一次系统调用 | ~μs 级 |
| .bak 备份 | 文件大小一次 copy | SSD ~100MB/s |
| **总开销** | | **<5ms<< LLM 秒级延迟)** |
---
## 八、与现有架构兼容性
| 维度 | 兼容性 |
|------|--------|
| tool_registry 注册 | ✅ 完全复用 write_file 模式 |
| RiskLevel 分流 | ✅ Medium → 审批(白名单收紧:纯读取外全审) |
| audit 审计日志 | ✅ process_tool_calls 自动入库 |
| validate_path | ✅ 复用黑名单 + canonicalize |
| .bak 备份 | ✅ 复用 FR-S7 已有逻辑 |
| ToolCard 前端渲染 | ✅ args 键值对自动适配 |
| 数据变更联动 AR-11 | ✅ emit df-data-changed 触发刷新 |
| 会话级授权 AE-04 | ✅ write_file 同类操作,可纳入 session_trust |
---
## 九、实施步骤
### 第一批(核心三件套,~50 行后端 + 前端零改动)
1. **`mod.rs``tool_registry.rs` 顶层**:加 `FILE_LOCKS` 静态 Mutex~10 行)
2. **`tool_registry.rs`**:注册 `patch_file` handler~40 行)
- 参数解析 + validate_path
- expected_hash 校验(可选,第一批可先加框架)
- 逐 patch 执行:扫描 old_text → 替换 → 记录结果
- .bak 备份(复用 write_file 逻辑)
- 截断输出stdout/stderr 各 10KB
3. **测试**AI 调用 `patch_file` 改一个已知文件,验证替换正确性
### 第二批(增强层,按需)
4. before/after_text 锁定逻辑(~15 行)
5. expected_hash 指纹校验完整接入(~10 行)
6. read_file 返回值扩展 file_hash 字段(~5 行)
### 第三批(远期)
7. replace_all 开关
8. regex 支持old_regex 字段)
9. undo stack会话内 patch 历史)
---
## 十、替代方案否决记录
| 方案 | 否决理由 |
|------|---------|
| sed/awk via run_command | B-37 stdout 空;修好后也是间接操作,无原子性/备份/审计 |
| write_file 全量覆盖 | 已出事故(762→248字节);大文件 token 浪费;无 diff |
| git apply (Git-based patch) | 强依赖 git 仓库;非 git 目录不可用 |
| JSON Patch (RFC 6902) | 面向 JSON/结构化数据;不适合自由格式文本 |
| AST-level (tree-sitter) | 过度工程;需每语言 parserAI 代码未必能 parse |
| 纯 line 号编辑 | 无内容校验=并发不安全;行号漂移易出错 |
| 全局文件队列 | 单用户桌面不需要Mutex 够用且简单 10x |