lili cb57b88866 docs(sse): 前端/后端 SSE 契约反向对齐 sse.ts 真实实现
studio src/lib/sse.ts 已是真实实现(connectAIStream/connectEventStream 双函数 +
fetch/ReadableStream + 按 event: 行分发),但三处正式文档仍停留在虚构契约,故反向对齐(反假绿):
- 前端-01 v6→v7:AI 流改两步(POST /ai/tasks 创建→GET /ai/tasks/{taskId}/stream 建流);
  事件流路径 /events/stream→/events;明确按 SSE event: 行分发。
- 后端-05 v8→v9:补 GET /ai/tasks/{taskId}/stream 端点 + SSE 事件契约表
  (chunk/quality_check/done/error 及各 payload);记 done 的 taskId/suggestionId
  后端 Long 序列化为 JSON 数字、与候选 uuid 字符串契约不一致(待后端统一,前端已在解析边界 String 归一)。
- dev-baseline/muse-studio/CLAUDE.md:SSE 章节由虚构 useAIStream/useEventStream hook
  改写为真实双函数 + 线格式 + 约束(AI 流不重连、事件流指数退避 1/2/5/10s、AbortController 关闭、无凭证 fail-closed)。
- sse.ts:onDone 注释补 WHY string(Long→JSON number→String 归一)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 02:55:24 -07:00

15 KiB
Raw Blame History

Muse Studio - 前端工程基线

项目概述

Muse Studio 是自研用户端创作工具,基于 React + Vite + TypeScript 构建的 SPA 应用。 核心能力包括AI 辅助创作编辑器、智能体工作台、知识库管理、创作市场、个人中心。

  • 框架React 18 + Vite + TypeScriptstrict 模式)
  • 状态管理TanStack Query服务端状态+ Zustand客户端状态+ React Hook Form + Zod表单
  • 编辑器Tiptap / ProseMirror
  • 样式Tailwind CSS
  • 测试Vitest + Testing Library + Playwright + MSW
  • 开发模式AI Agent 驱动开发

编码规范

TypeScript 严格模式

// tsconfig.json 关键配置
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
  }
}

中文注释要求

  1. 所有导出函数/组件/Hook 必须有中文 JSDoc
  2. 关键业务逻辑必须有详细中文行内注释
  3. Props 接口每个字段必须有中文注释
  4. 复杂算法、状态流转、副作用必须有中文说明
/**
 * 场景卡片组件
 * 展示单个创作场景的预览信息,支持点击进入编辑
 */
export function SceneCard({ scene, onSelect }: SceneCardProps) {
  // 计算场景字数统计(排除标点和空格)
  const wordCount = useMemo(() => countWords(scene.content), [scene.content]);
  // ...
}

Props 接口规范

/** 场景卡片属性 */
interface SceneCardProps {
  /** 场景数据对象 */
  scene: Scene;
  /** 选中场景时的回调,传入场景 ID */
  onSelect: (sceneId: string) => void;
  /** 是否处于只读模式,默认 false */
  readonly?: boolean;
  /** 自定义样式类名 */
  className?: string;
}

类型安全

  • 禁止 any,使用 unknown + 类型守卫
  • 禁止 as 类型断言(除非有充分注释说明安全性)
  • 禁止 @ts-ignore,使用 @ts-expect-error 并附带原因注释
  • 优先使用 discriminated union 而非可选字段
// 正确:类型守卫
function isApiError(error: unknown): error is ApiError {
  return (
    typeof error === 'object' &&
    error !== null &&
    'code' in error &&
    'message' in error
  );
}

组件规范

  • 函数组件 + Hooks,禁止 class 组件
  • 组件文件使用 PascalCaseSceneCard.tsx
  • Hook 文件使用 camelCaseuseSceneList.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                # 模块公开 APIbarrel export

状态管理

三层状态模型

层级 工具 适用场景
服务端状态 TanStack Query API 数据、缓存、同步、乐观更新
客户端状态 Zustand UI 状态、用户偏好、编辑器临时状态
表单状态 React Hook Form + Zod 表单输入、校验、提交

TanStack Query 规范

/** 获取场景列表 */
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 规范

/** 编辑器 UI 状态 */
interface EditorUIStore {
  /** 当前激活的面板 */
  activePanel: 'outline' | 'suggestions' | 'knowledge' | null;
  /** 是否显示 AI 建议浮层 */
  showSuggestions: boolean;
  /** 切换面板 */
  setActivePanel: (panel: EditorUIStore['activePanel']) => void;
}

export const useEditorUIStore = create<EditorUIStore>((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(<JSON>) 发送:事件类型在 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 正确关闭,避免连接泄漏。
  • 解析失败统一走 onErrorSSE_PARSE_ERROR),不让异常吞掉整条流。

编辑器规范

架构层级

Block场景/小节)→ Section段落→ Inline文本/标记)

自动保存策略

  • 触发:内容变更后 debounce 2 秒
  • 本地缓存:变更立即写入 IndexedDB防丢失
  • 远程同步debounce 后调用 API 保存
  • 冲突处理:基于版本号的 last-write-wins + 冲突提示
/**
 * 编辑器自动保存 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 必须定义完整的 schematoDOM、parseDOM
  • 插件Plugin按职责拆分单个插件不超过 200 行
  • AI 建议内容使用 Decoration 渲染,不修改文档结构

API 层规范

Hook 命名约定

use{Resource}{Action}

示例:
- useSceneList        → 获取场景列表
- useSceneDetail      → 获取场景详情
- useSceneCreate      → 创建场景
- useSceneUpdate      → 更新场景
- useSceneDelete      → 删除场景
- useAgentChat        → 智能体对话

请求客户端

// 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
  • 避免测试实现细节,关注用户行为和输出
describe('SceneCard 场景卡片', () => {
  it('应该正确显示场景标题和字数', () => {
    // 准备
    const scene = createMockScene({ title: '第一章', wordCount: 1500 });
    // 执行
    render(<SceneCard scene={scene} onSelect={vi.fn()} />);
    // 断言
    expect(screen.getByText('第一章')).toBeInTheDocument();
    expect(screen.getByText('1500 字')).toBeInTheDocument();
  });

  it('点击卡片时应该触发 onSelect 回调', async () => {
    const onSelect = vi.fn();
    render(<SceneCard scene={createMockScene()} onSelect={onSelect} />);
    await userEvent.click(screen.getByRole('button'));
    expect(onSelect).toHaveBeenCalledWith(expect.any(String));
  });
});

Lint 与格式化

工具链

  • ESLint:代码质量 + TypeScript 规则
  • Prettier:代码格式化
  • StylelintCSS/Tailwind 规则
  • husky + lint-staged:提交前自动检查

关键 ESLint 规则

{
  "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"] }]
  }
}

提交前检查

// 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 接口规范(前端对接依据)