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

14 KiB
Raw Blame History

前端合并审阅问题与修复复盘

审阅与修复日期:2026-08-29 涉及提交:f9efc4f,合并提交 c6c28e4。 文档用途:记录前端分支合并后暴露的问题域、形成原因、实际后果、修复思路和落地方案,供后续技术文档、比赛材料与博客写作使用。

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 没有配置 baseUrlpaths。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.vueEditorPane.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 显式完成转换,页面展示字段不反向污染后端请求。

解决方案

  • 增加 ApiNoteApiAgentRunApiSkillApiPluginApiProviderConfigApiTaskApiIndexStatus 等 Wire DTO
  • Notes Service 改用 foldermarkdown 和真实分页结构;
  • Search Service 将单值 UI Filter 转换为后端数组,并映射 items/page
  • Agent Service 使用 inputtool_timeout_secondsrun_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-04:SSE 跨网络分片丢失事件

原因

旧解析器把 eventNamedataStr 声明在每次 reader.read() 的循环内部。网络 Chunk 与 SSE Event 没有一一对应关系,一个事件的 event:data: 和结尾空行可以分别落在多个 Chunk 中。

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.valueMock 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. 验证

cd frontend
pnpm install --frozen-lockfile
pnpm build

cd ../backend
uv run pytest

cd ..
git diff --check

结果:

frontend production build passed
73 frontend modules transformed
62 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 没有真实渲染 写作与源码模式共用同一个 textareaChat 也把 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,并按真实路由名计算主导航选中项

安全边界:Markdown 解析结果不得直接使用未经清洗的 v-html。DOMPurify 是渲染链路的必需依赖,后续升级 marked 或允许扩展 Markdown 时也必须保留清洗步骤。