# Muse Studio - 前端工程基线 ## 项目概述 Muse Studio 是自研用户端创作工具,基于 React + Vite + TypeScript 构建的 SPA 应用。 核心能力包括:AI 辅助创作编辑器、智能体工作台、知识库管理、创作市场、个人中心。 - **框架**:React 18 + Vite + TypeScript(strict 模式) - **状态管理**:TanStack Query(服务端状态)+ Zustand(客户端状态)+ React Hook Form + Zod(表单) - **编辑器**:Tiptap / ProseMirror - **样式**:Tailwind CSS - **测试**:Vitest + Testing Library + Playwright + MSW - **开发模式**:AI Agent 驱动开发 --- ## 编码规范 ### TypeScript 严格模式 ```jsonc // tsconfig.json 关键配置 { "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true } } ``` ### 中文注释要求 1. **所有导出函数/组件/Hook 必须有中文 JSDoc** 2. **关键业务逻辑必须有详细中文行内注释** 3. **Props 接口每个字段必须有中文注释** 4. **复杂算法、状态流转、副作用必须有中文说明** ```tsx /** * 场景卡片组件 * 展示单个创作场景的预览信息,支持点击进入编辑 */ export function SceneCard({ scene, onSelect }: SceneCardProps) { // 计算场景字数统计(排除标点和空格) const wordCount = useMemo(() => countWords(scene.content), [scene.content]); // ... } ``` ### Props 接口规范 ```tsx /** 场景卡片属性 */ interface SceneCardProps { /** 场景数据对象 */ scene: Scene; /** 选中场景时的回调,传入场景 ID */ onSelect: (sceneId: string) => void; /** 是否处于只读模式,默认 false */ readonly?: boolean; /** 自定义样式类名 */ className?: string; } ``` ### 类型安全 - **禁止 `any`**,使用 `unknown` + 类型守卫 - **禁止 `as` 类型断言**(除非有充分注释说明安全性) - **禁止 `@ts-ignore`**,使用 `@ts-expect-error` 并附带原因注释 - 优先使用 discriminated union 而非可选字段 ```tsx // 正确:类型守卫 function isApiError(error: unknown): error is ApiError { return ( typeof error === 'object' && error !== null && 'code' in error && 'message' in error ); } ``` ### 组件规范 - **函数组件 + Hooks**,禁止 class 组件 - 组件文件使用 PascalCase:`SceneCard.tsx` - Hook 文件使用 camelCase:`useSceneList.ts` - 单个组件文件不超过 300 行,超过则拆分 - 组件职责单一,展示与逻辑分离 --- ## 目录结构 ``` src/ ├── app/ # 应用入口、路由配置、全局 Provider │ ├── routes/ # 路由定义(按功能模块组织) │ ├── providers/ # 全局 Context Provider │ └── App.tsx ├── components/ # 通用 UI 组件(与业务无关) │ ├── ui/ # 基础 UI 原子组件(Button, Input, Modal...) │ ├── layout/ # 布局组件(Sidebar, Header, Container...) │ └── feedback/ # 反馈组件(Toast, Loading, ErrorBoundary...) ├── features/ # 业务功能模块(核心开发区域) │ ├── editor/ # 编辑器模块(Tiptap 扩展、工具栏、协作) │ ├── agent/ # 智能体工作台(对话、编排、调试) │ ├── knowledge/ # 知识库管理(上传、索引、检索) │ ├── market/ # 创作市场(浏览、购买、发布) │ └── account/ # 个人中心(设置、订阅、数据) ├── hooks/ # 通用自定义 Hook ├── lib/ # 工具函数库(纯函数、无副作用) ├── stores/ # Zustand 全局状态 store ├── types/ # 全局类型定义 └── api/ # API 层(TanStack Query hooks) ├── client.ts # HTTP 客户端配置(拦截器、错误处理) ├── hooks/ # 按资源组织的 Query/Mutation hooks └── mocks/ # MSW mock handlers ``` ### Feature 模块内部结构 ``` features/editor/ ├── components/ # 模块内组件 ├── hooks/ # 模块内 Hook ├── stores/ # 模块内局部 store ├── types/ # 模块内类型 ├── utils/ # 模块内工具函数 └── index.ts # 模块公开 API(barrel export) ``` --- ## 状态管理 ### 三层状态模型 | 层级 | 工具 | 适用场景 | |------|------|----------| | 服务端状态 | TanStack Query | API 数据、缓存、同步、乐观更新 | | 客户端状态 | Zustand | UI 状态、用户偏好、编辑器临时状态 | | 表单状态 | React Hook Form + Zod | 表单输入、校验、提交 | ### TanStack Query 规范 ```tsx /** 获取场景列表 */ export function useSceneList(workId: string) { return useQuery({ queryKey: ['scenes', workId], queryFn: () => api.scenes.list(workId), staleTime: 30_000, // 30 秒内不重新请求 }); } /** 更新场景内容(乐观更新) */ export function useSceneUpdate() { const queryClient = useQueryClient(); return useMutation({ mutationFn: api.scenes.update, onMutate: async (newScene) => { // 乐观更新:立即更新本地缓存 await queryClient.cancelQueries({ queryKey: ['scenes', newScene.workId] }); const previous = queryClient.getQueryData(['scenes', newScene.workId]); queryClient.setQueryData(['scenes', newScene.workId], (old) => /* 更新逻辑 */); return { previous }; }, onError: (_err, _new, context) => { // 回滚:恢复之前的缓存 queryClient.setQueryData(['scenes', context?.previous]); }, }); } ``` ### Zustand 规范 ```tsx /** 编辑器 UI 状态 */ interface EditorUIStore { /** 当前激活的面板 */ activePanel: 'outline' | 'suggestions' | 'knowledge' | null; /** 是否显示 AI 建议浮层 */ showSuggestions: boolean; /** 切换面板 */ setActivePanel: (panel: EditorUIStore['activePanel']) => void; } export const useEditorUIStore = create((set) => ({ activePanel: null, showSuggestions: false, setActivePanel: (panel) => set({ activePanel: panel }), })); ``` --- ## SSE 实时通信规范 ### 双通道设计 studio 的 SSE 能力集中在 `src/lib/sse.ts`,对外暴露两个**函数**(既不是 React Hook,也不用浏览器原生 `EventSource`): 1. **AI Stream 通道**:`connectAIStream(url, handlers)` —— AI 生成内容的流式传输(独立连接,单次请求生命周期)。 2. **事件 Stream 通道**:`connectEventStream(lastEventId?)` —— 系统事件的统一推送(应用级长连接)。 > WHY 不用 `EventSource`:两条通道都要带自定义请求头(`X-API-Version`,事件流还要 `Authorization: Bearer`),而 `EventSource` 无法设置请求头,故统一用 `fetch` + `ReadableStream` 读流。 ### 线格式与事件分发(务必对齐后端) 后端用 Spring `SseEmitter.event().name(<事件名>).data()` 发送:**事件类型在 SSE `event:` 行,`data:` 行只是该事件的 payload JSON(不含 `type` 字段),`id:` 行承载可续传序号**。因此前端必须**按 `event:` 行分发**,禁止按 `data` 里的 `type` 字段分发。两条通道共用同一套「按 `event:` 行解析 + `id` / comment / 多行 `data` 拼接」的 parser。 AI Stream 的四类事件与 payload: | event | data payload | 说明 | |---|---|---| | `chunk` | `{ content, sequenceNo }` | Token 级增量文本 | | `quality_check` | `{ dimension, score, passed }` | 影子层质检结果 | | `done` | `{ taskId, suggestionId }` | 生成结束;`suggestionId` 是后续采纳写入 Canonical 的唯一入参 | | `error` | `{ code, message }` | 已脱敏错误,前端展示并关闭连接 | > WHY `done` 的 ID 归一为 string:后端 `taskId`/`suggestionId` 实为 Long、会序列化为 JSON **数字**,而前端全链路(采纳请求体、生成的 OpenAPI 类型)按 **string** 处理;故在 `done` 解析边界统一 `String()` 归一后再回调,避免数字流入采纳请求体。 ### 关键约束 - **AI Stream**:每次请求建立独立连接,用 `AbortController` 支持中止;**不自动重试/重连**(生成是一次性请求,失败走 `onError` 由用户重发)。 - **事件 Stream**:应用级长连接,断线按指数退避自动重连(`1s → 2s → 5s → 10s` 封顶),以收到的 `id` 作为 `lastEventId` cursor 续传;缺登录凭证时 fail-closed(不发空 Bearer)。 - 所有 SSE 连接必须在组件卸载时通过 `AbortController` 正确关闭,避免连接泄漏。 - 解析失败统一走 `onError`(`SSE_PARSE_ERROR`),不让异常吞掉整条流。 --- ## 编辑器规范 ### 架构层级 ``` Block(场景/小节)→ Section(段落)→ Inline(文本/标记) ``` ### 自动保存策略 - **触发**:内容变更后 debounce 2 秒 - **本地缓存**:变更立即写入 IndexedDB(防丢失) - **远程同步**:debounce 后调用 API 保存 - **冲突处理**:基于版本号的 last-write-wins + 冲突提示 ```tsx /** * 编辑器自动保存 Hook * 实现 IndexedDB 本地缓存 + 远程 debounce 保存 */ export function useAutoSave(editor: Editor, sceneId: string) { // 1. 监听 editor transaction // 2. 每次变更立即写入 IndexedDB // 3. debounce 2s 后调用远程保存 API // 4. 保存失败时保留 IndexedDB 数据,下次重试 } ``` ### ProseMirror Transaction 规范 - 所有编辑操作通过 Transaction 执行,禁止直接操作 DOM - 自定义 Node/Mark 必须定义完整的 schema(toDOM、parseDOM) - 插件(Plugin)按职责拆分,单个插件不超过 200 行 - AI 建议内容使用 Decoration 渲染,不修改文档结构 --- ## API 层规范 ### Hook 命名约定 ``` use{Resource}{Action} 示例: - useSceneList → 获取场景列表 - useSceneDetail → 获取场景详情 - useSceneCreate → 创建场景 - useSceneUpdate → 更新场景 - useSceneDelete → 删除场景 - useAgentChat → 智能体对话 ``` ### 请求客户端 ```tsx // api/client.ts // 统一 HTTP 客户端,基于 fetch 封装 // - 自动附加 Authorization header // - 统一错误格式转换(后端 ApiResponse → 前端 ApiError) // - 401 自动跳转登录 // - 请求/响应日志(开发环境) ``` ### MSW Mock 规范 - 所有 API Hook 必须有对应的 MSW handler - Mock 数据使用工厂函数生成(faker + 固定 seed) - Mock handler 放在 `src/api/mocks/handlers/` 按资源组织 - 开发环境默认启用 MSW,通过环境变量切换真实 API --- ## 测试规范 ### 覆盖率要求 | 类型 | 最低覆盖率 | |------|-----------| | 通用组件 | >= 75% | | 自定义 Hook | >= 80% | | 新增代码 | >= 85% | | 工具函数 | >= 90% | ### 测试分层 | 层级 | 工具 | 关注点 | |------|------|--------| | 单元测试 | Vitest | 工具函数、纯逻辑 | | 组件测试 | Vitest + Testing Library | 组件渲染、交互、状态 | | Hook 测试 | renderHook | 自定义 Hook 行为 | | E2E 测试 | Playwright | 用户完整流程 | ### 测试文件组织 ``` src/features/editor/components/ ├── SceneCard.tsx ├── SceneCard.test.tsx # 组件测试 └── __tests__/ └── SceneCard.e2e.ts # E2E 测试(可选) ``` ### 测试要求 - **新增组件/Hook 必须同时提交测试文件** - **测试用例使用中文描述** - 测试遵循 AAA 模式(Arrange-Act-Assert) - 避免测试实现细节,关注用户行为和输出 ```tsx describe('SceneCard 场景卡片', () => { it('应该正确显示场景标题和字数', () => { // 准备 const scene = createMockScene({ title: '第一章', wordCount: 1500 }); // 执行 render(); // 断言 expect(screen.getByText('第一章')).toBeInTheDocument(); expect(screen.getByText('1500 字')).toBeInTheDocument(); }); it('点击卡片时应该触发 onSelect 回调', async () => { const onSelect = vi.fn(); render(); await userEvent.click(screen.getByRole('button')); expect(onSelect).toHaveBeenCalledWith(expect.any(String)); }); }); ``` --- ## Lint 与格式化 ### 工具链 - **ESLint**:代码质量 + TypeScript 规则 - **Prettier**:代码格式化 - **Stylelint**:CSS/Tailwind 规则 - **husky + lint-staged**:提交前自动检查 ### 关键 ESLint 规则 ```jsonc { "rules": { "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/explicit-function-return-type": "warn", "react-hooks/exhaustive-deps": "error", "import/no-cycle": "error", "no-console": ["warn", { "allow": ["warn", "error"] }] } } ``` ### 提交前检查 ```jsonc // lint-staged 配置 { "*.{ts,tsx}": ["eslint --fix", "prettier --write"], "*.css": ["stylelint --fix", "prettier --write"], "*.{json,md}": ["prettier --write"] } ``` --- ## Git 工作流 ### 提交信息格式 ``` {类型}({模块}): {中文描述} 类型:feat | fix | refactor | style | test | docs | chore 模块:editor | agent | knowledge | market | account | common | api 示例: feat(editor): 实现场景自动保存与 IndexedDB 缓存 fix(agent): 修复 SSE 断线后未自动重连的问题 test(knowledge): 补充知识库上传组件单元测试 ``` ### 分支命名 ``` feature/{模块}-{描述} fix/{模块}-{描述} refactor/{模块}-{描述} 示例: feature/editor-auto-save fix/agent-sse-reconnect ``` --- ## Agent 行为约束 AI Agent 在本项目中开发时必须遵循以下规则: 1. **先读后写**:修改任何文件前,必须先读取该文件当前内容 2. **复用现有组件**:优先使用 `src/components/` 中已有的 UI 组件 3. **每次变更运行测试**:代码修改后必须运行 `pnpm vitest --run` 确认不破坏现有测试 4. **UI 变更验证**:涉及 UI 的修改必须启动 dev server 进行视觉验证 5. **不引入新依赖**:除非明确需要且经过确认,不自行添加 npm 包 6. **遵循现有模式**:新代码必须与同模块已有代码风格一致 7. **测试同步提交**:新增组件/Hook 必须同时编写并提交测试 8. **中文注释**:所有新增代码必须包含中文注释(JSDoc + 行内) --- ## 关键设计文档引用 开发时应参考以下设计文档(位于仓库根目录 `design-docs/`): - `前端-01-工程结构与核心依赖.md` — 工程架构、依赖选型、构建配置 - `前端-02-编辑器与影子层交互.md` — 编辑器核心架构、AI 影子层设计 - `前端-03-元引擎与动态表单.md` — 元数据驱动的动态 UI 生成 - `前端-04-市场与个人中心交互.md` — 市场浏览、交易、个人中心流程 - `前端-05-智能体与知识库工作台交互.md` — 智能体对话、知识库管理 - `后端-05-统一API契约-v1.md` — 后端 API 接口规范(前端对接依据)