fix(frontend): 修复合并审阅发现的构建与契约问题
恢复 Vue TypeScript 生产构建,补齐可运行页面壳子,并修复文件树与 SSE 状态问题。 按 FastAPI Wire Contract 统一 Service DTO 映射,同时补充前端开发说明和问题修复复盘。
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
# 前端合并审阅问题与修复复盘
|
||||
|
||||
> 审阅与修复日期: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<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. 验证
|
||||
|
||||
```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. 当前边界与后续事项
|
||||
|
||||
本轮修复解决了前端壳子的工程正确性和接口边界,不代表全部前端页面已经完成。后续仍需要:
|
||||
|
||||
- 实现 Search、Chat、Agent、Task、Skill、Plugin、Theme 和 Settings 页面;
|
||||
- 为 Service DTO 映射增加自动化契约测试;
|
||||
- 为 SSE Parser 增加跨 Chunk 单元测试;
|
||||
- 用 Tauri Command 替换 Mock Workspace Service;
|
||||
- 完成 Citation 定位、Agent Permission Dialog 和 Trace 可视化;
|
||||
- 在 CI 中加入前端构建和后端测试两个必需检查。
|
||||
@@ -0,0 +1,177 @@
|
||||
# 前端壳子与接口层开发说明
|
||||
|
||||
> 更新日期:2026-08-29
|
||||
> 适用范围:Vue 3 + TypeScript 前端壳子、Workspace、公共 Service、FastAPI 接口适配和 SSE。
|
||||
> 文档用途:帮助团队理解当前前端可用能力、模块边界、启动方式和后续页面开发入口。
|
||||
|
||||
## 1. 当前实现状态
|
||||
|
||||
本轮修复后,前端已经形成一条可安装、可类型检查、可生产构建的基础链路:
|
||||
|
||||
```text
|
||||
Vue Router
|
||||
→ App Shell
|
||||
→ Pinia Store
|
||||
→ Service / FastAPI DTO Adapter
|
||||
→ HTTP 或 SSE
|
||||
→ FastAPI
|
||||
```
|
||||
|
||||
当前可以使用的界面包括:
|
||||
|
||||
- Vault 入口页;
|
||||
- 应用标题栏、主侧边栏、辅助侧边栏和状态栏;
|
||||
- Workspace 文件树;
|
||||
- 基础 Markdown 文本编辑区;
|
||||
- 文件打开、新建、删除和重命名交互壳子;
|
||||
- 未完成模块的统一占位页。
|
||||
|
||||
Search、Chat、Agent、Task、Skill、Plugin、Theme 和 Settings 已保留稳定路由,但除公共 Store 与 Service 外,具体业务页面仍待后续实现。占位页用于保证主分支可构建、导航目标可识别,不代表对应功能页面已经验收。
|
||||
|
||||
## 2. 目录与职责
|
||||
|
||||
```text
|
||||
frontend/src/
|
||||
├── components/common/ App Shell 与公共导航组件
|
||||
├── contracts/index.ts UI View Model 与 FastAPI Wire DTO
|
||||
├── features/common/ 未实现功能的统一占位页
|
||||
├── features/editor/ 基础编辑器头部与文本编辑区
|
||||
├── features/vault/ Vault 入口
|
||||
├── features/workspace/ Workspace 与递归文件树
|
||||
├── router/index.ts 页面路由和 Vault Guard
|
||||
├── services/ HTTP、SSE、DTO 映射和模块 API
|
||||
├── stores/ Pinia 状态
|
||||
└── styles/tokens.css Design Token
|
||||
```
|
||||
|
||||
职责约定:
|
||||
|
||||
- Component 不直接拼接后端 URL;
|
||||
- Store 负责页面状态和业务操作编排;
|
||||
- Service 负责 HTTP/SSE 调用以及 Wire DTO 到 View Model 的转换;
|
||||
- `contracts/index.ts` 同时保留界面模型和以 `Api` 开头的 FastAPI DTO,两者不能混用;
|
||||
- OpenAPI `/openapi.json` 是后端 Wire Contract 的最终依据。
|
||||
|
||||
## 3. 路由与页面壳子
|
||||
|
||||
已注册路由:
|
||||
|
||||
```text
|
||||
/
|
||||
/workspace
|
||||
/search
|
||||
/chat
|
||||
/agent/runs/:runId?
|
||||
/tasks
|
||||
/extensions/skills
|
||||
/extensions/plugins
|
||||
/themes
|
||||
/settings
|
||||
```
|
||||
|
||||
除 Vault 入口外,其余路由需要先打开 Vault。尚未完成的页面统一加载 `PlaceholderView.vue`,后续开发时应逐个替换为真实页面组件,不要在路由中提前引用尚未提交的文件。
|
||||
|
||||
## 4. Workspace 与编辑器
|
||||
|
||||
Workspace 当前由以下组件构成:
|
||||
|
||||
```text
|
||||
WorkspaceView
|
||||
├── EditorHeader
|
||||
└── EditorPane
|
||||
|
||||
SecondarySidebar
|
||||
└── FileTreePanel
|
||||
└── FileTreeNode(递归)
|
||||
```
|
||||
|
||||
文件树把右键目标保存在 `contextTarget`,重命名和删除始终作用于实际被右键的节点,不再依赖当前编辑文件。根目录使用 `/` 表示,新增根级文件时直接写入 Store 顶层数组。
|
||||
|
||||
当前 `workspaceService` 仍是 Web 开发模式下的 Mock Adapter。保存、重命名和删除只保留调用边界,尚未接入 Tauri 文件系统命令。进入桌面端阶段后,应替换 Service 内部实现,不改变 Component 和 Store 的调用方式。
|
||||
|
||||
## 5. HTTP 接口层
|
||||
|
||||
公共请求由 `apiClient.ts` 处理:
|
||||
|
||||
- 支持 GET、POST、PUT、PATCH 和 DELETE;
|
||||
- 使用 `VITE_API_BASE_URL`,并兼容旧的 `VITE_API_BASE`;
|
||||
- 自动附加 `X-Request-Id`;
|
||||
- 将后端统一错误体转换为 `ApiErrorClass`;
|
||||
- 204 响应返回 `undefined`。
|
||||
|
||||
Service 已适配当前 FastAPI Contract:
|
||||
|
||||
| 模块 | 主要适配内容 |
|
||||
| --- | --- |
|
||||
| Notes | `folder`、`markdown`、直接 Note 响应和 `{items, page}` |
|
||||
| Search | 数组筛选字段、`items/page` 响应和 Search View Model 映射 |
|
||||
| Chat | `provider_id`、`model`、`messages` 和 ModelEvent SSE |
|
||||
| Agent | `input`、秒级 Timeout 字段、Run DTO 和 Permission Decision |
|
||||
| Skill / Plugin | 嵌套 `manifest`、安装 `package_path` 和 Plugin Permission PUT |
|
||||
| Provider | Provider Type、Capability 数组、模型列表包装和 Test 响应 |
|
||||
| Task | `due_at`、分页响应和当前后端支持字段 |
|
||||
| Index | `all/notes/vectors` Scope、Job 与状态 DTO |
|
||||
| System | `/health` 和 `/api/status` 的真实响应字段 |
|
||||
|
||||
界面模型中存在的展示字段不能直接发送给后端。例如 Task View Model 的 `priority` 和 `source` 当前只是界面层字段,Service 创建与更新请求不会把它们发送给不支持这些字段的 FastAPI Contract。
|
||||
|
||||
## 6. SSE
|
||||
|
||||
`SseClient` 同时服务于 Chat 和 Agent Event:
|
||||
|
||||
- 使用与普通 HTTP 相同的 API Base URL;
|
||||
- 支持 POST Chat Stream 和 GET Agent Event Stream;
|
||||
- 使用 `TextDecoder` 处理 UTF-8 增量字节;
|
||||
- 在网络分片之间保留 `event` 和多行 `data` 状态;
|
||||
- 以空行作为单个 SSE Event 的结束标志;
|
||||
- 识别 `Done`、`RunCompleted`、`RunFailed` 和 `RunCancelled`;
|
||||
- 支持 AbortController 主动取消。
|
||||
|
||||
Chat Store 已从定时器模拟输出切换为真实 `/api/chat` SSE。默认离线联调配置为:
|
||||
|
||||
```text
|
||||
provider_id = mock
|
||||
model = mock-1
|
||||
```
|
||||
|
||||
## 7. 环境和启动
|
||||
|
||||
```powershell
|
||||
cd frontend
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
联调前在另一个终端启动后端:
|
||||
|
||||
```powershell
|
||||
cd backend
|
||||
uv run uvicorn app.main:app --reload --host 127.0.0.1 --port 8000
|
||||
```
|
||||
|
||||
生产构建:
|
||||
|
||||
```powershell
|
||||
cd frontend
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 8. 当前验证基线
|
||||
|
||||
```text
|
||||
pnpm build passed
|
||||
uv run pytest 62 passed
|
||||
preview smoke HTTP 200
|
||||
git diff --check passed
|
||||
```
|
||||
|
||||
后端测试出现过一次 `.pytest_cache` 无法写入的 Windows 权限警告,不影响 62 项测试结果,也不涉及产品代码。
|
||||
|
||||
## 9. 后续开发要求
|
||||
|
||||
- 新页面文件与路由修改必须在同一提交中出现;
|
||||
- 新增或修改接口时同步更新 FastAPI DTO、Service 映射和接口文档;
|
||||
- 不允许用 `as any` 或错误返回类型掩盖 Contract 差异;
|
||||
- SSE 相关变更需要覆盖跨 Chunk、CRLF、多行 data、终态事件和取消;
|
||||
- Workspace 接入 Tauri 后,需要增加路径规范化、写入失败恢复和外部修改冲突测试;
|
||||
- Search、Agent、Skill、Plugin 等占位页应按功能逐个替换,不一次提交大量空页面。
|
||||
Reference in New Issue
Block a user