14 KiB
前端合并审阅问题与修复复盘
审阅与修复日期: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-01:TypeScript 与生产构建不可用
原因
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-04:SSE 跨网络分片丢失事件
原因
旧解析器把 eventName 和 dataStr 声明在每次 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.value,Mock Service 拼接根目录路径时只保留一个 /。
8. F-07:Chat 使用模拟流式输出
原因
Chat Store 已经存在 SSE Service,但发送消息后仍通过 setInterval 拼接固定文本,没有调用后端。
后果
- 后端 Provider、RAG、错误事件和取消无法通过前端验证;
- 页面看似工作,实际没有形成前后端链路;
- SSE 解析缺陷长期被 Mock 掩盖。
解决思路与方案
保留初始展示数据,但用户主动发送消息时调用真实 /api/chat。请求使用当前 Provider、Model、RAG 开关和消息历史;TextDelta 追加到 Assistant Message;Error、网络失败、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 没有真实渲染 | 写作与源码模式共用同一个 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 和 Error,Tool 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 只有预览卡没有 Token;Plugin 路由被错误映射为 Skill 激活状态 | 增加 Sepia Design Token,并按真实路由名计算主导航选中项 |
| F-18 | 文件切换仍可能丢失未保存内容 | loadFile 直接替换路径和正文,且旧的自动保存定时器会在切换后保存错误文件 |
切换前取消定时器,等待正在执行的保存并保存最新快照;保存失败或存在冲突时阻止切换;各入口仅在加载成功后更新 Workspace 活动路径 |
| F-19 | 编辑器外观启动时被默认值覆盖 | Theme Store 的立即监听早于 initTheme 执行,先把默认值写进 Local Storage |
增加 Hydration 状态,初始化前监听只更新 CSS,不持久化;读取本地配置完成后再允许写入 |
安全边界:Markdown 解析结果不得直接使用未经清洗的 v-html。DOMPurify 是渲染链路的必需依赖,后续升级 marked 或允许扩展 Markdown 时也必须保留清洗步骤。