Files
NotesAgentic/docs/retrospectives/前端合并审阅问题与修复复盘.md

285 lines
15 KiB
Markdown
Raw Permalink 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.
# 前端合并审阅问题与修复复盘
> 审阅与修复日期:2026-08-29
> 涉及提交:`f9efc4f`,合并提交 `c6c28e4`。
> 文档用途:记录前端分支合并后暴露的问题域、形成原因、实际后果、修复思路和落地方案,供后续技术文档、比赛材料与博客写作使用。
> 2026-09-02 状态补充:在本文多轮修复之后,项目又完成 Milkdown 写作工具栏、文件切换二次竞态修复、Shiki 只读高亮、Provider 预设/模型发现/加密 API Key 输入、智能体页面汉化,以及 Plugin Command/Settings Service。当前前端回归基线为 29 项测试,TypeScript 类型检查和生产构建通过。
## 1. 结论
原前端提交一次增加了 42 个文件和约 8000 行内容,但没有在提交前执行成功的生产构建。合并后同时存在工程配置、组件完整性、接口契约、流式协议和文件树状态五个问题域。
本轮处理结果:
| 编号 | 问题域 | 原级别 | 处理结果 |
| --- | --- | --- | --- |
| F-01 | TypeScript 与生产构建不可用 | P0 | 已修复,`pnpm build` 通过 |
| F-02 | 路由引用未提交页面 | P0 | 已改为统一占位页,并补充基础编辑器组件 |
| F-03 | 前后端 Contract 系统性漂移 | P1 | 已增加 Wire DTO 和显式 Service 映射 |
| F-04 | SSE 跨网络分片丢失事件 | P1 | 已重写增量解析状态机 |
| F-05 | 文件树右键操作目标错误 | P1 | 已改为保存实际右键节点 |
| F-06 | 根目录新增文件不可见且路径异常 | P2 | 已处理顶层插入与路径拼接 |
| F-07 | Chat 仍使用模拟流式输出 | P2 | 已接入真实 `/api/chat` SSE |
## 2. F-01TypeScript 与生产构建不可用
### 原因
Vite 配置了 `@` 指向 `src`,但 `tsconfig.app.json` 没有配置 `baseUrl``paths`。Vite 和 TypeScript 使用不同的模块解析配置,只配置其中一侧后,开发服务器可能暂时工作,`vue-tsc` 仍无法解析全部别名。
提交中还存在多个独立错误:
- `ComputedRef` 与字符串直接比较,缺少 `.value`
- `FileTreePanel.vue` 在两个 Script 中重复导入 `FileNode`
- 浏览器 ESM 代码调用 CommonJS `require()`
- Chat Store 使用不存在的 `conversation_id` 变量;
- Service 聚合文件导出不存在的 `ApiError`
- StatusBar 使用没有表达式的 `@click`
### 后果
- `pnpm build` 无法生成生产包;
- CI 无法验证前端;
- 别名错误产生的大量隐式 `any` 干扰真正错误定位;
- `main` 不再满足“可构建”要求。
### 解决思路
先恢复唯一可信的构建基线,再处理运行时问题。路径别名同时配置给 Vite 和 TypeScript,其余错误按 Vue 3 Composition API 和浏览器 ESM 规则逐项修复。
### 解决方案
-`tsconfig.app.json` 增加 `baseUrl``@/*` 映射;
- 在 Script 中通过 `.value` 读取 ComputedRef
- 拆分递归文件树组件,删除第二个 Script 和 `require()`
- 修正 Chat 变量名和类型导出;
- 删除无意义的空事件绑定;
-`pnpm build` 作为提交前强制检查。
## 3. F-02:路由和公共组件引用未提交文件
### 原因
路由表和 Secondary Sidebar 按最终页面结构一次性写完,但对应页面没有随提交进入仓库。Workspace 也引用了不存在的 `EditorHeader.vue``EditorPane.vue`。缺失项覆盖 Search、Chat、Agent、Task、Skill、Plugin、Theme、Settings 和多个 Sidebar Panel,共 21 个 Vue 文件。
### 后果
- 修复路径别名后,TypeScript 和 Vite 仍因模块不存在而失败;
- 开发者无法判断页面是遗漏提交,还是尚未实现;
- 后续成员可能分别创建同名但职责不同的组件。
### 解决思路
路由只能引用当前提交真实存在的组件。为保留产品信息架构,使用一个明确标注“功能开发中”的公共占位页,避免建立一批内容为空的伪页面。
### 解决方案
- 新增统一 `PlaceholderView.vue`
- 未实现功能路由暂时指向占位页;
- Secondary Sidebar 对未实现 Panel 显示说明文本;
- 增加可运行的基础 Editor Header 和 Textarea Pane
- 文档明确占位路由不代表业务页面完成。
## 4. F-03:前后端 Contract 系统性漂移
### 原因
前端先按页面需要定义了扁平 View Model,并直接把它们作为 HTTP 请求和响应类型。后端已经形成明确的 Pydantic Contract,包括分页包装、嵌套 Manifest、枚举和值对象,两边没有通过 OpenAPI 或人工核对完成同步。
典型差异:
| 模块 | 原前端假设 | FastAPI 实际 Contract |
| --- | --- | --- |
| Notes | `folder_path/content` | `folder/markdown` |
| Search | 单值筛选、`results/total` | 数组筛选、`items/page` |
| Agent | `task`、可选 Provider | `input`、Provider 与 Model 必填 |
| Permission | `allow + scope` | `allow_once/allow_session/deny` |
| Skill / Plugin | 扁平对象 | `manifest + runtime status` |
| Provider | Capability 对象 | Capability 数组 |
| Task | `due_date`、priority、source | `due_at`,后两项尚未进入后端 |
| Index | `full/fts/vector` | `all/notes/vectors` |
### 后果
- Agent 创建、Permission 响应等请求稳定返回 422;
- 列表接口拿到对象后被当成数组使用;
- Skill、Plugin 和 Provider 页面读取不到标识和能力;
- TypeScript 声称调用安全,但运行时结构完全不同;
- 捕获异常后回退 Mock 会掩盖真实联调失败。
### 解决思路
区分 Wire DTO 和 View Model。HTTP 边界严格使用与 FastAPI 一致的 `Api*` 类型,Service 显式完成转换,页面展示字段不反向污染后端请求。
### 解决方案
- 增加 `ApiNote``ApiAgentRun``ApiSkill``ApiPlugin``ApiProviderConfig``ApiTask``ApiIndexStatus` 等 Wire DTO
- Notes Service 改用 `folder``markdown` 和真实分页结构;
- Search Service 将单值 UI Filter 转换为后端数组,并映射 `items/page`
- Agent Service 使用 `input``tool_timeout_seconds``run_timeout_seconds`
- Permission Store 将 `allow + once/session` 转换为后端枚举;
- Skill 和 Plugin Service 展开嵌套 Manifest
- 增加 Plugin Permission PUT
- Provider Capability 数组转换为界面布尔 Map;
- Task Service 只发送后端支持字段,并转换 `due_date/due_at`
- Index Service 显式转换 Scope
- 默认离线 Provider ID 统一为后端的 `mock`
## 5. F-04SSE 跨网络分片丢失事件
### 原因
旧解析器把 `eventName``dataStr` 声明在每次 `reader.read()` 的循环内部。网络 Chunk 与 SSE Event 没有一一对应关系,一个事件的 `event:``data:` 和结尾空行可以分别落在多个 Chunk 中。
```text
Chunk 1: event: TextDelta\n
Chunk 2: data: {"event":"TextDelta", ...}\n\n
```
读取 Chunk 2 时事件名已被重置成 `message`。如果 data 与空行分开,data 内容也会丢失。
### 后果
- Chat 增量文本偶发不显示;
- Agent 终态事件无法触发完成回调;
- 问题受网络分片影响,开发机难以稳定复现;
- 长回答和远程 Provider 更容易出现错误。
### 解决思路
SSE 解析状态必须跨 Chunk 保存,以完整行和空行结束事件为边界,不能以单次网络读取为边界。
### 解决方案
- 将 Event Name 和 Data Lines 移到读取循环外;
- Buffer 只移除已经形成完整行的内容;
- 支持 LF、CRLF、多行 data 和注释行;
- 流结束时 flush TextDecoder 和剩余 Event
- 终态回调增加去重;
- SSE URL 复用普通 HTTP 的 Base URL 解析。
## 6. F-05:文件树右键操作目标错误
### 原因
右键菜单打开时保存了 `contextMenuPath`,但执行删除和重命名时读取的是 `workspaceStore.activeFile`。右键节点与当前编辑节点是两个独立状态。
### 后果
用户右键未激活文件并点击删除时,可能关闭或修改正在编辑的另一个文件。这属于潜在数据破坏问题。
### 解决思路
菜单操作必须绑定菜单打开时的目标对象,不能在点击命令时从无关的 Active State 推断。
### 解决方案
- 使用 `contextTarget: Ref<FileNode | null>` 保存右键节点;
- Rename 和 Delete 只消费 `contextTarget`
- 菜单关闭后清空目标;
- 将递归 Node 独立为 `FileTreeNode.vue`,通过类型化 Emit 向上传递节点。
## 7. F-06:根目录新增文件不可见且路径异常
### 原因
Store 把 `/` 当作普通父节点查找,但文件树没有代表根目录的虚拟节点。Service 直接使用 `folderPath + '/' + name` 拼接路径,根目录会得到 `//name.md`
### 后果
- Service 返回成功,但新建项目没有加入界面文件树;
- 打开的文件路径带双斜杠;
- 接入真实文件系统后可能产生平台间路径差异。
### 解决思路与方案
顶层数组本身就是根节点的 children。当 `parentPath``/` 或空字符串时直接写入 `fileTree.value`Mock Service 拼接根目录路径时只保留一个 `/`
## 8. F-07Chat 使用模拟流式输出
### 原因
Chat Store 已经存在 SSE Service,但发送消息后仍通过 `setInterval` 拼接固定文本,没有调用后端。
### 后果
- 后端 Provider、RAG、错误事件和取消无法通过前端验证;
- 页面看似工作,实际没有形成前后端链路;
- SSE 解析缺陷长期被 Mock 掩盖。
### 解决思路与方案
保留初始展示数据,但用户主动发送消息时调用真实 `/api/chat`。请求使用当前 Provider、Model、RAG 开关和消息历史;TextDelta 追加到 Assistant MessageError、网络失败、Done 和主动取消同步更新 Streaming State。默认使用离线 `mock / mock-1`,无需外部 API Key。
## 9. 验证
```powershell
cd frontend
pnpm install --frozen-lockfile
pnpm build
cd ../backend
uv run pytest
cd ..
git diff --check
```
结果:
```text
frontend production build passed
73 frontend modules transformed
71 backend tests passed
preview returned HTTP 200
git diff --check passed
```
## 10. 预防措施
- PR 创建前必须执行与 CI 相同的 `pnpm build`
- 路由只引用当前提交存在的文件;
- FastAPI `/openapi.json` 是 Wire Contract 的唯一事实来源;
- View Model 与 API DTO 分层,Service 必须显式转换;
- 不用 Mock Fallback 掩盖 4xx、5xx 和契约错误;
- SSE 测试按任意 Chunk 边界构造数据,不能假定一次 read 等于一次 Event
- 删除、移动和覆盖等高影响操作必须携带明确目标 ID 或对象;
- 合并后如果发现 P0,先恢复主分支构建,再继续业务页面开发。
## 11. 当前边界与后续事项
首次修复解决了前端壳子的工程正确性和接口边界。之后已继续补齐全部业务路由页面;Tauri Host、Stronghold 和真实文件系统仍属于桌面集成阶段。后续仍需要:
- 为 Service DTO 映射增加自动化契约测试;
- 为 SSE Parser 增加跨 Chunk 单元测试;
- 用 Tauri Command 替换 Mock Workspace Service
- 在 CI 中加入前端构建和后端测试两个必需检查。
## 12. 全页面完成后的第二轮审阅与修复
全部页面接通后再次审阅,发现编译通过并不等于交互状态正确。本轮问题与处理如下:
| 编号 | 问题 | 原因与后果 | 解决方案 |
| --- | --- | --- | --- |
| F-08 | Markdown 没有真实渲染 | 写作与源码模式共用同一个 `textarea`Chat 也把 Markdown 当纯文本显示 | 使用 `marked` 解析 GFM,使用 DOMPurify 清洗 HTML;编辑页提供源码输入和实时预览,Chat 回答复用安全渲染器 |
| F-09 | 重命名后路径仍是旧值 | 只修改节点名称,没有同步节点、子节点、打开文件和编辑器路径,后续保存或删除会作用于旧路径 | 新增递归路径迁移,同时更新 `openFiles`、活动路径和 Editor 当前路径;Mock 内容缓存也随路径迁移 |
| F-10 | 删除活动文件后正文错位 | Workspace 切换了活动文件,但 Editor 仍保留已删除文件正文 | 删除文件或文件夹时统一清理其所有打开路径;若存在下一个文件则加载,否则关闭编辑器 |
| F-11 | 异步读取和保存存在竞态 | 快速切换文件可能让较早请求覆盖较新文件;保存过程中继续编辑会被错误标记为已保存 | 使用读取版本号丢弃过期结果;保存使用路径和正文快照,只有快照仍是最新内容时才标记 `saved` |
| F-12 | Agent 权限弹窗跨 Run 残留 | 切换 Run、ToolResult 和终态事件没有释放 PermissionRequest | 加载 Run 前清空请求,并在 ToolResult、Completed、Failed、Cancelled 时同步清理;同时更新本地 Run 状态 |
| F-13 | Chat 丢弃非文本 SSE 事件 | Store 只处理 TextDelta 和 ErrorTool Call 请求会留下空消息 | 增加 Thinking、ToolCall Start/Delta/End、Usage 和 Citation 状态处理及页面卡片展示 |
| F-14 | Task 字段表现为保存但实际丢失 | 前端展示后端不支持的 Priority/Source,更新请求又漏掉后端已支持的 `note_id` | 暂时移除不可持久化字段的编辑与筛选;补齐 `note_id` 更新与解除关联的 `null` 语义 |
| F-15 | 编辑器设置不生效 | Settings Store 与 Editor/Theme 没有联动,自动保存固定为 1500ms | 自动保存、默认模式、拼写检查、字号、行高和行宽改为实际驱动编辑器,并保存到 Local Storage |
| F-16 | Vector 降级提示永远不可达 | `vectorUnavailable` 只声明不赋值,向量错误直接清空结果 | 对明确的向量、Embedding、模型和 Provider 不可用错误自动重试 FTS,并显示降级状态 |
| F-17 | 护眼主题与 Plugin 导航状态异常 | Sepia 只有预览卡没有 TokenPlugin 路由被错误映射为 Skill 激活状态 | 增加 Sepia Design Token,并按真实路由名计算主导航选中项 |
| F-18 | 文件切换仍可能丢失未保存内容 | `loadFile` 直接替换路径和正文,且旧的自动保存定时器会在切换后保存错误文件 | 切换前取消定时器,等待正在执行的保存并保存最新快照;保存失败或存在冲突时阻止切换;各入口仅在加载成功后更新 Workspace 活动路径 |
| F-19 | 编辑器外观启动时被默认值覆盖 | Theme Store 的立即监听早于 `initTheme` 执行,先把默认值写进 Local Storage | 增加 Hydration 状态,初始化前监听只更新 CSS,不持久化;读取本地配置完成后再允许写入 |
安全边界:Markdown 解析结果不得直接使用未经清洗的 `v-html`。DOMPurify 是渲染链路的必需依赖,后续升级 `marked` 或允许扩展 Markdown 时也必须保留清洗步骤。
## 13. 写作、Provider 与智能体页面的后续修复
第三轮交互完善继续处理了文件切换、Markdown 选区格式、代码块默认状态、亮暗主题对比度和浮动工具栏失效问题。Provider 设置页增加 OpenAI、DeepSeek、Ollama 预设与模型自动发现,API Key 改为提交给后端加密保存,不进入 Pinia 或 Local Storage。智能体页面的运行状态、事件、工具、权限及导航文案已完成中文化,同时保留技术 ID 便于排障。
该轮新增 Store、Workspace、文件树、编辑器和中文标签回归测试;该轮当时结果为前端 14 项、后端 71 项测试通过,生产构建通过。最新全仓基线见本文开头的状态补充。