docs: 巡检简报+todo 回写(2026-06-15 第2轮)
巡检发现:
- 2196c77 workflow 整文件替换回退破坏(AiChat+Ideas 12项功能)
- B-260615-03 truncated 标志已落地
- AR-8-scroll scrollToBottom smooth 已补
- CR-260615-09 .ai-md 残余:4详情页各21处 scoped .ai-md
(全局 ai-md.css 75行已建,旧副本待清理但非阻塞)
This commit is contained in:
295
docs/02-架构设计/aichat交互体验改进方案-2025-07-15.md
Normal file
295
docs/02-架构设计/aichat交互体验改进方案-2025-07-15.md
Normal file
@@ -0,0 +1,295 @@
|
||||
# AIChat 交互体验改进方案
|
||||
|
||||
> 创建: 2025-07-15 | 状态: 待讨论
|
||||
> 范围: 消息发送、流式渲染、对话管理、错误恢复、技能/Provider、窗口布局等非授权类交互
|
||||
|
||||
---
|
||||
|
||||
## 一、消息输入与发送
|
||||
|
||||
### 1.1 无法编辑已发送消息
|
||||
|
||||
**现象**:用户发出消息后发现措辞有误,只能重新打一条新消息。无法像 ChatGPT/Claude 那样编辑上一条用户消息并重新生成。
|
||||
|
||||
**根因**:`sendMessage`(useAiSend.ts)push 后的消息是只追加不可变的。前端没有编辑入口,后端 `ai_chat_send` 也没有"替换最后一条 user 消息并重跑"的语义。
|
||||
|
||||
**方案**:
|
||||
- 用户消息气泡 hover 显示"编辑"按钮
|
||||
- 点击后消息内容回填到输入框,用户修改后发送时后端截断该消息之后的所有历史(含 AI 回复),重新跑 agentic loop
|
||||
- 后端新增 `ai_chat_edit` 命令,接收 `message + 截断位置`,替换 messages 数组中对应 user 消息并清掉后续消息,然后走正常 `run_agentic_loop`
|
||||
|
||||
### 1.2 无法重新生成 AI 回复
|
||||
|
||||
**现象**:AI 回答不满意时没有"重新生成"按钮,只能重新措辞追问。
|
||||
|
||||
**根因**:`AiChat.vue` 的 AI 消息气泡上没有任何操作按钮。后端也没有 `ai_regenerate` 命令。
|
||||
|
||||
**方案**:
|
||||
- AI 消息气泡 hover 显示操作栏(复制 | 重新生成)
|
||||
- "重新生成":后端删除最后一条 AI 消息,用倒数第二条 user 消息重新触发 `run_agentic_loop`
|
||||
- 后端新增 `ai_regenerate` 命令
|
||||
|
||||
### 1.3 无法一键复制消息内容
|
||||
|
||||
**现象**:代码或文本只能手动选中复制。流式渲染中选中文字会被后续 delta 打断。
|
||||
|
||||
**根因**:消息气泡上没有复制按钮。
|
||||
|
||||
**方案**:
|
||||
- AI 消息气泡 hover 显示"复制"按钮
|
||||
- 点击后 `navigator.clipboard.writeText(msg.content)`,toast 提示"已复制"
|
||||
- 代码块单独提供"复制代码"按钮(hover 代码块右上角浮出)
|
||||
|
||||
### 1.4 缺少 `@` 实体引用
|
||||
|
||||
**现象**:用户无法在输入时引用某个项目、任务或文件。描述需求时无法精确指定上下文,AI 可能猜错对象。
|
||||
|
||||
**根因**:输入框只有 `/` 技能联想,没有 `@` 实体引用机制。
|
||||
|
||||
**方案**:
|
||||
- 输入框支持 `@` 触发实体联想浮层(复用技能联想的 popover 架构)
|
||||
- 联想源:项目列表、任务列表、最近编辑的文件
|
||||
- 选中后在消息中展开为 `[项目: u-desk]` 等标记文本,后端 system prompt 注入对应实体的上下文摘要
|
||||
|
||||
### 1.5 输入框高度过紧
|
||||
|
||||
**现象**:textarea 最大高度 120px(约 5-6 行),超过后内部滚动。用户写长提示词时看不到全貌。
|
||||
|
||||
**根因**:`autoResize` 中 `Math.min(el.scrollHeight, 120)` 限制太紧。
|
||||
|
||||
**方案**:
|
||||
- 最大高度提升到 200px(约 10 行),超过后再内部滚动
|
||||
- 或改为可拖拽调整高度(底部 resize handle)
|
||||
|
||||
---
|
||||
|
||||
## 二、流式渲染与消息展示
|
||||
|
||||
### 2.1 流式渲染中无法稳定选中文字
|
||||
|
||||
**现象**:AI 正在流式输出时,用户尝试选中已渲染的文字,新的 delta 触发 DOM 更新导致选区丢失。
|
||||
|
||||
**根因**:`renderContent` 在流式时返回 `streamingHtml.value`(rAF 节流重 parse),每次更新都 `v-html` 替换整个 DOM 子树,浏览器选区被清除。
|
||||
|
||||
**方案**:
|
||||
- 方案 A(推荐):检测到用户正在选择文字时(`selectionchange` 事件 + 选区非空且在消息容器内),暂停 rAF 流式 parse,选区结束后恢复
|
||||
- 方案 B:流式渲染时在已完成的块(blockCache 命中的块)上使用独立 DOM 节点不参与 v-html 替换,仅末块动态更新
|
||||
|
||||
### 2.2 代码块无语法高亮、无复制按钮
|
||||
|
||||
**现象**:Markdown 代码块只有纯文本渲染,没有语法高亮,也没有一键复制按钮。
|
||||
|
||||
**根因**:`useMarkdown` composable 配置了 marked + DOMPurify,但没有集成 highlight.js / Prism 等高亮器。
|
||||
|
||||
**方案**:
|
||||
- 集成 highlight.js(体积小、语言全),在 marked renderer 的 `code` 回调中调用 `hljs.highlightAuto` 或按 info string 指定语言
|
||||
- 代码块右上角浮出"复制"按钮(纯前端,hover 显示)
|
||||
- 高亮器按需加载(与 marked 一样后台预热,不阻塞首屏)
|
||||
|
||||
### 2.3 历史消息无分页懒加载
|
||||
|
||||
**现象**:切换对话时 `switchConversation` 一次性加载全部 messages 到 `state.messages`。超长对话可能一次加载几百条消息。
|
||||
|
||||
**根因**:`switchConversation`(useAiConversations.ts)直接 `state.messages = rawMsgs.filter().map()`,无分页。
|
||||
|
||||
**方案**:
|
||||
- 首次加载最近 50 条,滚动到顶部时加载更多(前端 slice + 后端支持 offset/limit 查询 messages)
|
||||
- 或前端全量加载但虚拟滚动只渲染可视区域(见 7.2)
|
||||
|
||||
### 2.4 消息不显示时间戳
|
||||
|
||||
**现象**:消息气泡上不显示发送时间。用户无法判断某条消息是多久前发的。
|
||||
|
||||
**根因**:模板中 AI/用户消息都没有渲染 `msg.timestamp`。
|
||||
|
||||
**方案**:
|
||||
- 消息气泡下方或侧边以极小字号(9px)+ dim 颜色展示相对时间(`formatRelativeZh`)
|
||||
- hover 时 tooltip 展示完整时间
|
||||
|
||||
---
|
||||
|
||||
## 三、对话管理
|
||||
|
||||
### 3.1 对话搜索缺失
|
||||
|
||||
**现象**:侧栏对话列表只能滚动浏览,没有搜索框。对话多了之后找不到特定对话。
|
||||
|
||||
**根因**:侧栏 `.ai-conv-list` 直接渲染 `groupedActive`,上方只有"新建"按钮,没有搜索输入框。
|
||||
|
||||
**方案**:
|
||||
- 侧栏 header 下方增加搜索输入框(实时过滤 `state.conversations`,匹配 title)
|
||||
- 搜索时取消分组,按相关度/时间平铺展示
|
||||
- 支持 `Ctrl+K` 快捷键聚焦搜索框
|
||||
|
||||
### 3.2 对话无法置顶
|
||||
|
||||
**现象**:没有置顶或收藏功能。重要对话会被新对话挤到下面。
|
||||
|
||||
**根因**:对话列表只按 `updated_at` 排序,无 `pinned` 字段。
|
||||
|
||||
**方案**:
|
||||
- `ai_conversations` 表增加 `pinned INTEGER DEFAULT 0`
|
||||
- 排序逻辑改为 `pinned DESC, updated_at DESC`
|
||||
- 侧栏对话项 hover 显示"置顶/取消置顶"按钮(图钉图标)
|
||||
|
||||
### 3.3 对话无法导出
|
||||
|
||||
**现象**:无法将对话导出为 Markdown / JSON / 文本文件。
|
||||
|
||||
**根因**:没有导出 IPC 命令和前端入口。
|
||||
|
||||
**方案**:
|
||||
- 后端新增 `ai_conversation_export(conv_id, format)` 命令,支持 `markdown` / `json` / `txt` 三种格式
|
||||
- 前端侧栏对话项 hover 显示"导出"按钮,或对话操作菜单中提供
|
||||
- Markdown 格式:`## 用户` / `## 助手` 交替,代码块保留围栏
|
||||
|
||||
### 3.4 新建对话强制中断已有生成
|
||||
|
||||
**现象**:用户在 AI 生成中点"新建对话",后端 `ai_conversation_create` 会强制复位 `generating=false` + 置 `stop_flag`,中断当前生成。
|
||||
|
||||
**根因**:`commands.rs ai_conversation_create` 中 `if session.generating` 分支主动结束生成(B-260615-10 设计)。
|
||||
|
||||
**方案**:
|
||||
- 生成中点"新建对话"时弹 ConfirmDialog:"当前对话正在生成,确定要新建对话并中断吗?"
|
||||
- 用户确认后才执行中断+新建;取消则不操作
|
||||
- 或改为不中断:新对话仅切换视图,旧对话后台继续生成(需 AiSession 多实例化支持,属远期方案)
|
||||
|
||||
---
|
||||
|
||||
## 四、错误处理与恢复
|
||||
|
||||
### 4.1 错误气泡无操作入口
|
||||
|
||||
**现象**:错误气泡只显示一行文字(如 `[GLM-4] AI 调用失败(HTTP 401): Unauthorized`),没有"重试"按钮或"去设置"链接。
|
||||
|
||||
**根因**:`AiError` 事件只携带 `error: String`,前端 `handleEvent` push 一条 `isError: true` 的消息气泡,无操作按钮。
|
||||
|
||||
**方案**:
|
||||
- 错误气泡底部增加操作按钮区:
|
||||
- **重试**:取上一条 user 消息重新发送(调 `ai_chat_send`)
|
||||
- **去设置**(仅 401/403/Provider 未配置时显示):跳转到 Settings → AI Tab
|
||||
- 错误消息结构扩展:后端 `AiError` 增加 `error_type: Option<ErrorType>` 枚举(`auth` / `network` / `timeout` / `provider_config` / `unknown`),前端据此决定显示哪些按钮
|
||||
|
||||
### 4.2 流式中断丢失已生成文本
|
||||
|
||||
**现象**:网络波动导致流式中断(idle timeout 或 mid-stream error),已接收的文本被丢弃(`stream_llm` return None → `agentic.rs` emit AiError 并退出),用户看到错误气泡,之前生成的几百字全部丢失。
|
||||
|
||||
**根因**:`stream_llm` 遇到错误时 `return None`,不保留已接收的部分文本。`agentic.rs` 收到 None 后直接结束。
|
||||
|
||||
**方案**:
|
||||
- `stream_llm` mid-stream error 时改为返回 `Some((partial_text, tool_calls, usage))` + 一个 `incomplete: bool` 标志
|
||||
- `agentic.rs` 收到 incomplete=true 时:
|
||||
- 已有文本正常入库(标注 `truncated`)
|
||||
- emit `AiCompleted`(而非 AiError)让前端正常展示已生成内容
|
||||
- 在消息末尾追加系统提示:"⚠ 响应因网络中断不完整"
|
||||
- 前端 AI 消息气泡底部显示"继续生成"按钮(用最后一条 user 消息重新触发,后端识别不完整消息做续写或重跑)
|
||||
|
||||
---
|
||||
|
||||
## 五、技能与 Provider
|
||||
|
||||
### 5.1 Provider 切换零反馈
|
||||
|
||||
**现象**:点击 provider bar 循环切换 provider(`cycleProvider`),切换后只在 bar 上显示名字变化,没有 toast 或动画反馈。
|
||||
|
||||
**根因**:`cycleProvider` 直接调 `store.setProvider`,无 UI 反馈。
|
||||
|
||||
**方案**:
|
||||
- 切换后 toast 提示"已切换到 {providerName}"
|
||||
- provider bar 切换时加 0.15s 淡入动画
|
||||
- bar 上增加 provider 状态指示(model 名称小字展示)
|
||||
|
||||
### 5.2 技能联想不展示参数用法
|
||||
|
||||
**现象**:技能联想浮层显示 name + description + source,但不显示技能的参数格式或示例。选中后 placeholder 里的 `argument_hint` 太短。
|
||||
|
||||
**根因**:浮层设计只展示概要信息,`argument_hint` 仅在选中后作为 placeholder 显示。
|
||||
|
||||
**方案**:
|
||||
- 联想浮层每项增加一行参数提示(`argument_hint` 以等宽字体小字展示)
|
||||
- 选中技能后输入框上方 chip 展示完整的参数格式说明(而非仅 name + description)
|
||||
- 或选中技能后自动在输入框填入模板骨架(如 `/commit `),光标定位到参数位置
|
||||
|
||||
---
|
||||
|
||||
## 六、窗口与布局
|
||||
|
||||
### 6.1 侧栏宽度不可调
|
||||
|
||||
**现象**:侧栏固定 160px,不可拖拽调整宽度。长对话标题被截断。
|
||||
|
||||
**根因**:`.ai-conv-sidebar { width: 160px; min-width: 160px; }` 硬编码。
|
||||
|
||||
**方案**:
|
||||
- 侧栏右边缘增加 2px 拖拽条(`cursor: col-resize`)
|
||||
- 拖拽时实时更新 width,范围 120~280px
|
||||
- 宽度持久化到 `df-ai-ui` 设置(与 sidebarOpen/maximized 同存)
|
||||
|
||||
### 6.2 分离窗口关闭后生成态不同步
|
||||
|
||||
**现象**:分离窗口关闭时,如果正在生成,主窗口虽然 `panelOpen=true` 恢复,但 `state.streaming` / `state.currentText` 可能不同步——如果生成中的对话不是主窗口当前活跃对话,主窗口看不到生成态。
|
||||
|
||||
**根因**:分离窗口关闭走 `closeDetachedWindow`,清了 localStorage 快照,但主窗口的状态依赖事件路由自然恢复。
|
||||
|
||||
**方案**:
|
||||
- 分离窗口关闭时,主窗口检测 `state.generatingConvId` 非空,自动切换到正在生成的对话
|
||||
- 或在主窗口 header 显示"对话 X 正在生成中"提示条,点击切换过去
|
||||
|
||||
---
|
||||
|
||||
## 七、其他交互细节
|
||||
|
||||
### 7.1 无键盘快捷键
|
||||
|
||||
**现象**:除了 Enter 发送 / Shift+Enter 换行,没有其他快捷键。
|
||||
|
||||
**方案**:
|
||||
|
||||
| 快捷键 | 功能 |
|
||||
|--------|------|
|
||||
| `Ctrl+N` | 新建对话 |
|
||||
| `Ctrl+K` | 搜索对话 |
|
||||
| `Ctrl+L` | 清空当前对话 |
|
||||
| `Ctrl+Shift+C` | 复制最后一条 AI 消息 |
|
||||
| `Ctrl+R` | 重新生成最后一条 AI 回复 |
|
||||
| `Esc` | 关闭面板(嵌入模式)/ 关闭窗口(分离模式) |
|
||||
| `Ctrl+B` | 切换侧栏 |
|
||||
|
||||
### 7.2 消息列表无虚拟滚动
|
||||
|
||||
**现象**:消息列表直接 `v-for` 渲染所有消息,长对话几百条消息全量渲染 DOM,滚动卡顿。
|
||||
|
||||
**根因**:没有使用虚拟滚动库。
|
||||
|
||||
**方案**:
|
||||
- 集成 `vue-virtual-scroller` 或自研 IntersectionObserver 懒渲染
|
||||
- 仅渲染可视区域 ±缓冲区的消息节点
|
||||
- 注意:流式渲染的最后一条消息需要始终保持挂载
|
||||
|
||||
### 7.3 空状态无引导
|
||||
|
||||
**现象**:首次打开 AI Chat(有 provider 配置),空状态只显示标题+提示语,没有示例问题或快捷操作。
|
||||
|
||||
**方案**:
|
||||
- 空状态展示 3-4 个示例问题卡片(如"帮我创建一个新项目"、"查看当前任务列表"、"分析这段代码的问题")
|
||||
- 点击卡片自动填入输入框并发送
|
||||
- 无 provider 配置时展示"去配置 AI Provider"引导按钮
|
||||
|
||||
### 7.4 对话标题生成对用户不透明
|
||||
|
||||
**现象**:对话标题由后端 `ensure_conversation_title` 异步生成,侧栏对话从"新对话"突然变成某个标题,没有过渡。
|
||||
|
||||
**方案**:
|
||||
- 标题生成后加 0.3s 淡入动画
|
||||
- 或在标题前加小图标标识"AI 自动生成"(可 hover 查看)
|
||||
|
||||
---
|
||||
|
||||
## 八、落地优先级
|
||||
|
||||
| 批次 | 痛点 | 理由 |
|
||||
|------|------|------|
|
||||
| **第一批** | 2.1 流式选中文字、1.2+1.3 消息操作栏(复制/重新生成)、4.1 错误重试、4.2 断线保文 | 每次对话都会遇到,最高频痛点 |
|
||||
| **第二批** | 2.2 代码高亮+复制、3.1 对话搜索、7.1 键盘快捷键、3.4 新建不中断生成 | 显著提升日常效率 |
|
||||
| **第三批** | 1.1 编辑重发、1.4 `@` 引用、3.3 导出、7.2 虚拟滚动、6.1 侧栏可调 | 按需推进,锦上添花 |
|
||||
| **第四批** | 2.3 分页加载、2.4 时间戳、3.2 置顶、5.1+5.2 技能/Provider 反馈、7.3+7.4 空状态/标题 | 打磨细节 |
|
||||
282
docs/02-架构设计/aichat授权体验改进方案-2025-07-15.md
Normal file
282
docs/02-架构设计/aichat授权体验改进方案-2025-07-15.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# AIChat 授权功能体验改进方案
|
||||
|
||||
> 创建: 2025-07-15 | 状态: 待讨论
|
||||
|
||||
## 一、当前授权机制概览
|
||||
|
||||
### 1.1 核心流程
|
||||
|
||||
```
|
||||
用户发送消息
|
||||
→ Agentic Loop (最多 10 轮)
|
||||
→ LLM 流式响应
|
||||
→ 解析 tool_calls
|
||||
→ 按风险等级分流:
|
||||
├─ Low Risk → 自动并行执行(join_all)
|
||||
├─ Medium → 暂停循环,等待人工审批
|
||||
└─ High → 暂停循环,等待人工审批
|
||||
→ 审批通过 → 执行工具 → try_continue_agent_loop 恢复循环
|
||||
→ 审批拒绝 → tool_result="用户拒绝了此操作" → 恢复循环让 LLM 自行决策
|
||||
```
|
||||
|
||||
### 1.2 工具风险分级矩阵
|
||||
|
||||
| 风险等级 | 工具 | 执行方式 |
|
||||
|---------|------|---------|
|
||||
| **Low** | `list_projects`, `list_tasks`, `list_ideas`, `list_trash`, `read_file`, `list_directory` | 自动执行,并行 `join_all` |
|
||||
| **Medium** | `create_project`, `create_task`, `create_idea`, `update_project`, `update_task`, `bind_directory`, `write_file` | 需人工审批 |
|
||||
| **High** | `delete_project`, `restore_project`, `purge_project`, `delete_task`, `run_workflow`, `run_command` | 需人工审批 |
|
||||
|
||||
### 1.3 已实现的亮点
|
||||
|
||||
- **审计留痕**:`ai_tool_executions` 表完整记录每次工具调用的状态、风险等级、决策者、请求时间和执行时间
|
||||
- **断点恢复**:应用重启后通过 `restore_pending_approvals()` 从数据库重建内存中的待审批状态
|
||||
- **审批卡片可读化**:`build_approval_reason()` 解析工具参数,拼接人类可读的审批理由
|
||||
- **generating 状态 RAII 守护**:`GeneratingGuard` 确保无论正常退出还是 panic,`generating` 标志都能复位
|
||||
- **数据变更联动刷新**:工具执行成功后自动 emit `df-data-changed` 事件,前端自动刷新列表
|
||||
- **安全防护**:API Key 存 OS keyring、文件路径双层校验、write_file 覆盖前自动 .bak 备份 + 原子写
|
||||
|
||||
### 1.4 关键代码位置
|
||||
|
||||
| 模块 | 文件 | 职责 |
|
||||
|------|------|------|
|
||||
| 工具注册表 | `src-tauri/src/commands/ai/tool_registry.rs` | 工具定义 + 风险等级 + handler 同源 |
|
||||
| 审批处理 | `src-tauri/src/commands/ai/audit.rs` | 工具调用审计 + pending 审批恢复 + 审批后状态回填 |
|
||||
| Agentic 循环 | `src-tauri/src/commands/ai/agentic.rs` | 流式接收 → 工具执行 → 结果回传 LLM → 循环 |
|
||||
| IPC 命令 | `src-tauri/src/commands/ai/commands.rs` | `ai_approve` / `ai_chat_send` / `ai_chat_stop` 等 |
|
||||
| 审批卡片 UI | `src/components/ToolCard.vue` | 卡片渲染 + 参数展示 + 批准/拒绝按钮 |
|
||||
| 卡片列表 | `src/components/ToolCardList.vue` | 多卡片折叠管理 |
|
||||
| 聊天面板 | `src/components/AiChat.vue` | 消息列表 + 审批转发 + 输入区 |
|
||||
| AI Store | `src/stores/ai.ts` | 模块级单例 state |
|
||||
| 发送/审批 | `src/composables/ai/useAiSend.ts` | `approveToolCall` 乐观更新 + IPC 调用 |
|
||||
|
||||
---
|
||||
|
||||
## 二、用户体验痛点分析
|
||||
|
||||
### 痛点 1:逐个审批,无法批量操作
|
||||
|
||||
**现象**:当 AI 一次返回多个 Medium/High 工具调用时(比如同时创建 3 个任务 + 写 2 个文件),用户需要逐个点批准/拒绝。
|
||||
|
||||
**根因**:`process_tool_calls`(audit.rs)把每个待审批工具都推入 `pending_approvals` HashMap,前端为每个 pending 渲染独立的 ToolCard,每个卡片只有自己的"批准/拒绝"按钮。`ToolCardList.vue` 没有批量操作入口。
|
||||
|
||||
**影响**:高频交互场景下审批变成体力活,用户体验疲劳。
|
||||
|
||||
### 痛点 2:审批阻塞整个对话,期间无法输入
|
||||
|
||||
**现象**:审批等待期间 `generating=true`,用户虽然能上滑看历史,但无法发送新消息(`sendMessage` 会入队),也无法预知还有多少审批在排队。
|
||||
|
||||
**根因**:`ai_chat_send`(commands.rs:46)检查 `session.generating` 为 true 时拒绝新消息。审批等待期间 `generating` 保持 true(`agentic.rs` 的 `guard.disarm()` 保持 true 以便 `try_continue` 续生成)。
|
||||
|
||||
**影响**:用户处于"被动等待"状态,无法并行做其他事。
|
||||
|
||||
### 痛点 3:Medium 和 High 体验无差异
|
||||
|
||||
**现象**:两者都弹同样的审批卡片、同样的按钮,交互流程完全一致。
|
||||
|
||||
**根因**:`process_tool_calls`(audit.rs:103-120)中 Medium 和 High 走同一个 `pending_approvals.insert` 分支,区别仅在 `build_approval_reason` 的文案后缀("请确认是否执行" vs "高风险,需人工批准")。
|
||||
|
||||
**影响**:High 操作(如 `purge_project` 不可恢复)缺乏足够的警示力度,容易误操作。
|
||||
|
||||
### 痛点 4:write_file 审批信息不够直观
|
||||
|
||||
**现象**:`write_file` 是 Medium 风险,审批卡片只展示 path 和 content 参数。content 可能是几百行代码,在审批卡片里以 `formatArgValue` 截断到 300 字符展示。
|
||||
|
||||
**根因**:`ToolCard.vue` 的 `toolArgsEntries` → `displayArgValue` → `formatArgValue` 对超长值统一截断到 300 字符。
|
||||
|
||||
**影响**:用户无法看清要写入的完整内容,只能盲目批准。尤其覆盖已有文件时,用户不知道会改什么。
|
||||
|
||||
### 痛点 5:审批后无进度反馈
|
||||
|
||||
**现象**:点击"批准"后,卡片乐观置 `running`,但用户不知道:
|
||||
- 工具正在执行还是已执行完等待 LLM 续生成
|
||||
- 还有多少待审批在排队
|
||||
- 整个 agentic 循环进行到第几轮
|
||||
|
||||
**根因**:前端没有全局的 agentic 循环进度指示器。`AiAgentRound` 事件虽然通知了轮次,但没有在 UI 上持久化展示。
|
||||
|
||||
**影响**:用户对系统状态缺乏掌控感,尤其在多轮工具调用时。
|
||||
|
||||
### 痛点 6:审批卡片可能被滚出视口
|
||||
|
||||
**现象**:当消息很多时,审批卡片可能被新消息推到上方滚出视口。
|
||||
|
||||
**根因**:`AiChat.vue` 的 `collapseInactive` 不会收起 `pending_approval` 卡片,但也不会自动滚动到 pending 卡片。没有全局徽标提醒。
|
||||
|
||||
**影响**:用户可能看不到待审批项,对话看起来"卡住了"但不知道在等什么。
|
||||
|
||||
---
|
||||
|
||||
## 三、改进方案
|
||||
|
||||
### P0 — 快速改善体感
|
||||
|
||||
#### 3.1 批量审批
|
||||
|
||||
**方案**:当同一轮有多个 pending 时,在 ToolCardList 顶部显示"全部批准(N) / 全部拒绝"按钮。
|
||||
|
||||
**改动范围**:
|
||||
- `ToolCardList.vue`:新增批量操作栏,监听 toolCalls 中 pending_approval 数量
|
||||
- `useAiSend.ts`:新增 `approveAll(rejectAll)` 方法,循环调用 `ai_approve`
|
||||
|
||||
**交互**:
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ ⏳ 3 项待审批 │
|
||||
│ [✓ 全部批准] [✕ 全部拒绝] │
|
||||
├─────────────────────────────────┤
|
||||
│ [工具卡片 1 - pending] │
|
||||
│ [工具卡片 2 - pending] │
|
||||
│ [工具卡片 3 - pending] │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 3.2 审批计数器 + 跳转
|
||||
|
||||
**方案**:在输入框上方或 header 显示 `⏳ 2 项待审批`,点击跳转到第一个 pending 卡片。
|
||||
|
||||
**改动范围**:
|
||||
- `AiChat.vue`:header 区域增加审批徽标
|
||||
- `ToolCardList.vue`:暴露 `scrollToFirstPending` 方法
|
||||
|
||||
**交互**:
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ 🤖 助手 ⏳2 [+][×] │ ← 徽标在 header
|
||||
├──────────────────────────────────┤
|
||||
│ ...消息列表... │
|
||||
│ ┌─ 工具卡片 (pending) ──┐ │ ← 点击徽标滚动到此
|
||||
│ │ 创建任务:XXX │ │
|
||||
│ │ [批准] [拒绝] │ │
|
||||
│ └────────────────────────┘ │
|
||||
├──────────────────────────────────┤
|
||||
│ [输入框] │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 3.3 write_file diff 预览
|
||||
|
||||
**方案**:write_file 审批时,如果文件已存在,展示前后对比 diff 而非裸 content。
|
||||
|
||||
**改动范围**:
|
||||
- 后端 `tool_registry.rs`:write_file handler 在执行前读取旧文件内容,返回 diff 信息(或前端请求 diff)
|
||||
- `ToolCard.vue`:pending_approval + name=write_file 时渲染 diff 视图
|
||||
|
||||
**交互**:
|
||||
```
|
||||
┌─ 写入文件 src/main.rs (待审批) ──────┐
|
||||
│ │
|
||||
│ - fn main() { │ ← 红色:删除行
|
||||
│ - println!("hello"); │
|
||||
│ + fn main() { │ ← 绿色:新增行
|
||||
│ + println!("hello, world"); │
|
||||
│ + setup_logging(); │
|
||||
│ │
|
||||
│ ⚠ 覆盖已有文件 (23→45 行) │
|
||||
│ [查看完整内容] [批准] [拒绝] │
|
||||
└───────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### P1 — 增强控制力
|
||||
|
||||
#### 3.4 自动审批策略(信任模式)
|
||||
|
||||
**方案**:Settings 中增加"自动审批"配置,用户可选择对特定风险等级或工具类型自动放行。
|
||||
|
||||
**配置项**:
|
||||
```
|
||||
Settings → AI → 自动审批策略
|
||||
○ 严格模式(默认):所有 Medium/High 需人工审批
|
||||
○ 宽松模式:Medium 自动放行,High 需人工审批
|
||||
○ 自定义:按工具类型选择
|
||||
☑ write_file(workspace 内自动放行)
|
||||
☑ create_task / create_idea(自动放行)
|
||||
☐ delete_*(始终需审批)
|
||||
☐ run_command(始终需审批)
|
||||
```
|
||||
|
||||
**改动范围**:
|
||||
- `df-storage`:`app_settings` 表存储配置(KV 已有 V13 表)
|
||||
- `audit.rs`:`process_tool_calls` 读取配置,决定 Medium 工具是否进 pending 或直接执行
|
||||
- `Settings.vue`:新增配置面板
|
||||
|
||||
#### 3.5 High 二次确认
|
||||
|
||||
**方案**:delete/purge/run_command 等高风险操作,批准后弹出二次确认。
|
||||
|
||||
**改动范围**:
|
||||
- `ToolCard.vue`:High 风险 + approved=true 时,先弹 inline 确认("确定要永久删除?此操作不可恢复")
|
||||
- 或用现有 `ConfirmDialog` 组件
|
||||
|
||||
**交互**:
|
||||
```
|
||||
第一次点击"批准":
|
||||
→ 卡片内弹出确认提示
|
||||
→ "确定要永久删除项目「XXX」?此操作不可恢复"
|
||||
→ [确认删除] [取消]
|
||||
|
||||
第二次点击"确认删除":
|
||||
→ 才真正执行 ai_approve
|
||||
```
|
||||
|
||||
#### 3.6 审批超时
|
||||
|
||||
**方案**:可配置超时自动拒绝(默认 5 分钟),避免对话永久卡住。
|
||||
|
||||
**改动范围**:
|
||||
- 后端:`AiSession` 增加 pending 审批的 `created_at` 时间戳,定时检查超时
|
||||
- 或前端:`useAiSend.ts` 在 pending 时启动定时器,超时自动调 `ai_approve(id, false)`
|
||||
|
||||
---
|
||||
|
||||
### P2 — 信息透明度
|
||||
|
||||
#### 3.7 Agentic 进度条
|
||||
|
||||
**方案**:在消息区域底部显示循环进度。
|
||||
|
||||
**改动范围**:
|
||||
- `AiChat.vue`:底部增加进度指示条
|
||||
- `useAiEvents.ts`:处理 `AiAgentRound` 事件时更新进度
|
||||
|
||||
**交互**:
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ ...消息列表... │
|
||||
│ │
|
||||
│ 🔄 循环 3/10 · ⏳2待审批 · ✅5完成 │ ← 底部进度条
|
||||
├──────────────────────────────────┤
|
||||
│ [输入框] │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 3.8 审批历史面板
|
||||
|
||||
**方案**:独立页面展示 `ai_tool_executions` 表的审计记录。
|
||||
|
||||
**改动范围**:
|
||||
- 后端:新增 `list_tool_executions` IPC 命令(查询审计表)
|
||||
- 前端:新增 `AuditLog.vue` 视图,表格展示历史记录
|
||||
|
||||
**展示字段**:
|
||||
| 时间 | 工具 | 风险 | 状态 | 决策者 | 参数摘要 | 结果摘要 |
|
||||
|------|------|------|------|--------|---------|---------|
|
||||
|
||||
---
|
||||
|
||||
## 四、实施优先级建议
|
||||
|
||||
| 优先级 | 改进项 | 预估工作量 | 用户价值 |
|
||||
|--------|--------|-----------|---------|
|
||||
| **P0** | 3.1 批量审批 | 0.5 天 | 🔥🔥🔥 |
|
||||
| **P0** | 3.2 审批计数器 + 跳转 | 0.5 天 | 🔥🔥🔥 |
|
||||
| **P0** | 3.3 write_file diff 预览 | 1 天 | 🔥🔥 |
|
||||
| **P1** | 3.4 自动审批策略 | 1.5 天 | 🔥🔥🔥 |
|
||||
| **P1** | 3.5 High 二次确认 | 0.5 天 | 🔥 |
|
||||
| **P1** | 3.6 审批超时 | 0.5 天 | 🔥 |
|
||||
| **P2** | 3.7 Agentic 进度条 | 0.5 天 | 🔥🔥 |
|
||||
| **P2** | 3.8 审批历史面板 | 1 天 | 🔥 |
|
||||
|
||||
**建议第一批落地**:P0 三项(批量审批 + 计数器 + diff 预览),总计约 2 天工作量,覆盖最高频的体验痛点。
|
||||
360
docs/02-架构设计/patch_file工具设计-2026-06-15.md
Normal file
360
docs/02-架构设计/patch_file工具设计-2026-06-15.md
Normal file
@@ -0,0 +1,360 @@
|
||||
# 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 请求结构
|
||||
|
||||
```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 通过 → 执行 patch(L2 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) | 过度工程;需每语言 parser;AI 代码未必能 parse |
|
||||
| 纯 line 号编辑 | 无内容校验=并发不安全;行号漂移易出错 |
|
||||
| 全局文件队列 | 单用户桌面不需要;Mutex 够用且简单 10x |
|
||||
@@ -322,6 +322,12 @@
|
||||
- **治理方向**:① 后端支持阶段流转(planning→in_progress…);② 前端 map 对齐后端真实值(active/deleted/archived);③ 拆双字段(status 生命周期 + stage 开发阶段)。
|
||||
- **状态**:📐 待治理(治标已落地,根本病根未除)
|
||||
|
||||
### i18n import 统一 `@/i18n` 路径别名 [2026-06-15]
|
||||
- **决策**:vite.config.ts 加 `resolve.alias.{ '@': '/src' }` 路径别名;全项目 i18n import 统一为 `import i18n from '@/i18n'`,替代相对路径 `../i18n`/`../../i18n`。
|
||||
- **原因/取舍**:CR-08 i18n 批量改造时,workflow 代理将 stores/ 下层文件 i18n import 从相对路径(`../../i18n`)改为错误多层的 `../../../i18n` 或正确的 `@/i18n`(但项目无别名配置),导致 vite 构建失败(Could not resolve)。相对路径随文件深度变化易断(stores/ 两层 vs composables/ai/ 三层 vs utils/ 一层);`@/i18n` 绝对路径不随文件位置变化,零维护成本;Vue/Vite 生态标准做法(多数 Vue 项目默认配 `@` → src);别名仅影响构建时解析,运行时无开销。
|
||||
- **影响范围**:9 个源文件(import 侧) + 1 个配置文件(vite.config.ts)。
|
||||
- **状态**:✅ 已落地(commit d6eb855 + 6254d06)
|
||||
|
||||
## 状态持久化
|
||||
|
||||
### 窗口位置/大小:用 tauri-plugin-window-state(纯 Rust 层)
|
||||
@@ -481,6 +487,14 @@
|
||||
- **「决策」术语边界**(2026-06-12):devflow 语境「决策/决策需求点」= 日常开发功能细节取舍(为什么这么定),**非** aichat 决策能力升级(B 路线 coordinator/conditions)。后者属架构层,记 Phase2计划/模块文档,不混入本文档。
|
||||
- **代码审查甄别原则**(2026-06-13):审查发现问题时,按「运行时失败/数据损坏 → 简单清理 → 记录不动 → 不做」四档甄别。当前项目规模下,list_all 无 LIMIT、ALLOWED_COLUMNS 不分表、bool→int 重复等属「记录不动」——个人工具表不超千行,加分页/拆白名单是过度设计,维护成本 >> 收益。原则:**真实 bug 修、简单清理做、规模不到位的优化先不动**,保持全局简洁和扩展容易。
|
||||
|
||||
## 文档维护
|
||||
|
||||
### 文档历史项保留原则:标状态不删行 [2026-06-15]
|
||||
- **决策**:所有文档(ARCHITECTURE.md + 模块文档)中的历史设计项**一律保留原文不删除**,仅在行末或旁注标注实现状态:`✅ 已实现` / `❌ 未实现(设计预留)` / `⚠️ 骨架空壳(有文件但无实质逻辑)` / `~~已删~~`(R-PD-X 等重构决策引用)。
|
||||
- **原因/取舍**:全量核对报告(2026-06-15)发现 ARCHITECTURE.md 含多出虚构/过时项(ModelRouter 已删/Docker 等 5 节点未实现)。初版方案为「删虚构行+注」,用户两次明确否决删方案,要求保留全部历史项+标状态。理由:① 保留设计演进痕迹,接手方可理解"曾经考虑过什么、为什么没做";② 删除会导致核对报告等交叉引用断链;③ 标状态列比删行信息量更大。
|
||||
- **影响范围**:DOC-260615-01~14 全部文档修项均遵循此原则。已落地:ARCHITECTURE.md §5.4 ModelRouter 标 `❌ ~~已删~~` + §5.5 8 节点标 `✅` / `❌`(commit be38a44)。
|
||||
- **状态**:✅ 2026-06-15 落地(用户两次否决删方案后定稿,首批标注已 commit)
|
||||
|
||||
**相关文档**:
|
||||
- [Phase 1 架构决策](./Phase1架构决策-2026-06-12.md) — 架构级决策(ADR)
|
||||
- [经验记录](./经验记录-2026-06-14.md) — 踩坑/约定/技巧/bug 排查教训
|
||||
|
||||
Reference in New Issue
Block a user