Files
DevFlow/docs/02-架构设计/专项设计/patch_file工具设计-2026-06-15.md
绝尘 998a2f243d 文档: 架构方案文档(意图识别论证+多主题愿景/论证+文档物理分类+边界清晰化)
squash合并:
- 意图识别层论证(8维度+10业界佐证)
- 多主题上下文管理愿景+并存论证+补充论证(多轮agentic)
- 架构设计文档物理分类(四子目录+INDEX+命名规范+引用同步+边界清晰化)
- 前端架构技术债清单归档
2026-06-19 15:04:04 +08:00

14 KiB
Raw Blame History

Patch File 工具设计

创建: 2026-06-15 | 状态: 设计定稿待实施 | 优先级: 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 请求结构

/// 局部文件更新请求
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 响应结构

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 返回值新增字段:

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 同一文件

实现

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 + sizeDevFlow 是本地桌面应用,「外部修改」= 用户切 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.rstool_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 改一个已知文件,验证替换正确性

第二批(增强层,按需)

  1. before/after_text 锁定逻辑(~15 行)
  2. expected_hash 指纹校验完整接入(~10 行)
  3. read_file 返回值扩展 file_hash 字段(~5 行)

第三批(远期)

  1. replace_all 开关
  2. regex 支持old_regex 字段)
  3. 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