# 前端合并审阅问题与修复复盘 > 审阅与修复日期: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 中。 ```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` 保存右键节点; - 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. 验证 ```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 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 时也必须保留清洗步骤。