# P2: muse-studio 用户端搭建 — 执行计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. **目标:** 从零搭建 muse-studio React SPA,实现写作台、作品管理、知识库、智能体、市场、个人中心 6 个功能域。 **架构:** Vite + React + TypeScript SPA,TanStack Query 数据层,Zustand 状态管理,Tiptap/ProseMirror 编辑器,MSW Mock API,SSE 双通道实时通信,IndexedDB 安全网。 **技术栈:** React 18, TypeScript 5, Vite 6, TanStack Query 5, Zustand 4, React Hook Form 7 + Zod 3, Tiptap 2, Tailwind CSS 3, MSW 2, Vitest + Testing Library + Playwright --- ## 执行状态(2026-05-25) **当前提交范围:** `muse-studio/` 用户端 SPA 已完成 6 个功能域的可交互实现:作品工作区、写作台、知识库工作台、智能体工作台、创作市场、个人中心。 **已完成基础设施:** - Vite + React 18 + TypeScript 工程骨架。 - React Router 应用路由与侧边栏布局。 - TanStack Query 数据层、Zustand 编辑器状态、统一 API Client。 - MSW Mock API,覆盖 content、ai、knowledge、market、account。 - Tiptap 编辑器、IndexedDB 草稿安全网、SSE AI 流式生成、候选 diff 与采纳流程。 **已完成业务域:** - Feature 1 写作台:编辑器、AI 生成面板、候选对比与采纳。 - Feature 2 我的作品 + 工作台:作品列表、创建、删除、章节大纲、新建章节、删除章节、切章重载。 - Feature 3 知识库工作台:按当前产品规格演进为知识库列表、资料管理、上传、手写条目、外链导入、解析状态轮询、删除/卸载。 - Feature 4 智能体工作台:智能体列表、创建、试用、作品开放槽位预检与绑定。 - Feature 5 市场 + 个人中心:市场搜索筛选、授权获取、安装;账户资料、权益配额、用量摘要。 **验证记录:** - `pnpm exec tsc -b --pretty false` 通过。 - `pnpm exec vitest run` 通过:7 个测试文件、23 个测试。 - `pnpm build` 通过;仍有 Vite chunk > 500KB 提示,后续可做路由级 code splitting。 - `pnpm lint` 退出码 0;仅 `public/mockServiceWorker.js` 生成文件有 unused eslint-disable warning。 - 已用 Playwright + 本机 Chrome 冒烟 `/agents`、`/market`、`/account`,页面非空且无明显加载失败或重叠。 **未完成或后续增强:** - 尚未补 Playwright E2E 自动化用例。 - 尚未按完成标准统计组件/Hook 覆盖率百分比。 - `public/mockServiceWorker.js` lint warning 建议通过生成文件 ignore 处理,不手改生成文件。 ## Step 1: 工程脚手架 ### Task 1.1: 初始化 Vite + React + TS 项目 **仓库路径:** `muse-studio/`(新建) - [ ] **Step 1: 创建项目** ```bash pnpm create vite@latest muse-studio --template react-ts cd muse-studio ``` - [ ] **Step 2: 安装核心依赖** ```bash pnpm add react@18 react-dom@18 react-router-dom@6 pnpm add @tanstack/react-query@5 pnpm add zustand@4 pnpm add react-hook-form@7 zod@3 @hookform/resolvers@3 pnpm add @tiptap/react @tiptap/pm @tiptap/starter-kit @tiptap/extension-placeholder pnpm add -D tailwindcss@3 postcss autoprefixer pnpm add -D @types/react @types/react-dom pnpm add -D vitest@2 @testing-library/react @testing-library/jest-dom jsdom pnpm add -D eslint@9 prettier @typescript-eslint/parser pnpm add -D msw@2 pnpm add -D playwright@1 @playwright/test pnpm add -D start-server-and-test ``` - [ ] **Step 3: 配置 Vite** ```typescript // muse-studio/vite.config.ts import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import path from 'path' export default defineConfig({ plugins: [react()], resolve: { alias: { '@': path.resolve(__dirname, 'src'), }, }, server: { port: 5173, proxy: { '/app-api': { target: 'http://localhost:48080', changeOrigin: true, }, }, }, test: { globals: true, environment: 'jsdom', setupFiles: ['./src/test/setup.ts'], css: true, }, }) ``` - [ ] **Step 4: 创建目录结构** ```bash mkdir -p muse-studio/src/{app/routes,components/{ui,layout,feedback},features/{editor,agent,knowledge,market,account},hooks,stores,lib,types,api/{hooks,mocks/handlers},test} ``` - [ ] **Step 5: 配置 Tailwind** ```bash npx tailwindcss init -p ``` ```javascript // muse-studio/tailwind.config.js export default { content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], theme: { extend: {} }, plugins: [], } ``` - [ ] **Step 6: 提交** ```bash git init && git add -A && git commit -m "feat(init): 初始化 Vite + React + TS 项目脚手架" ``` ### Task 1.2: 路由框架 + 布局 **文件:** - 创建: `src/app/routes/index.tsx` - 创建: `src/components/layout/AppLayout.tsx` - 创建: `src/pages/{Workspace,Editor,Knowledge,Agent,Market,Account}Page.tsx` - [ ] **Step 1: 编写路由配置** ```tsx // src/app/routes/index.tsx import { createBrowserRouter } from 'react-router-dom'; import AppLayout from '@/components/layout/AppLayout'; import WorkspacePage from '@/pages/WorkspacePage'; import EditorPage from '@/pages/EditorPage'; import KnowledgePage from '@/pages/KnowledgePage'; import AgentPage from '@/pages/AgentPage'; import MarketPage from '@/pages/MarketPage'; import AccountPage from '@/pages/AccountPage'; export const router = createBrowserRouter([ { path: '/', element: , children: [ { index: true, element: }, { path: 'works/:workId', element: }, { path: 'works/:workId/editor/:chapterId', element: }, { path: 'knowledge', element: }, { path: 'knowledge/:workId', element: }, { path: 'agents', element: }, { path: 'market', element: }, { path: 'account', element: }, ], }, ]); ``` - [ ] **Step 2: 编写布局组件** ```tsx // src/components/layout/AppLayout.tsx import { Outlet } from 'react-router-dom'; import Sidebar from './Sidebar'; export default function AppLayout() { return (
); } ``` - [ ] **Step 3: 实现侧边栏导航** ```tsx // src/components/layout/Sidebar.tsx import { NavLink } from 'react-router-dom'; const navItems = [ { to: '/', label: '我的作品', icon: '📝' }, { to: '/knowledge', label: '知识库', icon: '📚' }, { to: '/agents', label: '智能体', icon: '🤖' }, { to: '/market', label: '市场', icon: '🏪' }, { to: '/account', label: '个人中心', icon: '👤' }, ]; export default function Sidebar() { return ( ); } ``` - [ ] **Step 4: 创建占位页面** ```tsx // src/pages/WorkspacePage.tsx export default function WorkspacePage() { return

我的作品

; } ``` - [ ] **Step 5: Main.tsx 集成路由** ```tsx // src/main.tsx import React from 'react'; import ReactDOM from 'react-dom/client'; import { RouterProvider } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { router } from './app/routes'; import './index.css'; const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 30_000, retry: 1, }, }, }); ReactDOM.createRoot(document.getElementById('root')!).render( ); ``` - [ ] **Step 6: 验证启动** ```bash cd muse-studio && pnpm dev ``` 浏览 `http://localhost:5173`,验证路由导航正常。 - [ ] **Step 7: 提交** ```bash git add -A && git commit -m "feat(router): 添加路由框架 + 布局组件 + 6 个功能域占位" ``` --- ## Step 2: 核心基础设施 ### Task 2.1: API 客户端 + 类型集成 **文件:** - 创建: `src/api/client.ts` - 创建: `src/types/openapi.ts`(从 OpenAPI 生成物导入) - [ ] **Step 1: 创建 API 客户端** ```typescript // src/api/client.ts import { Base } from '@/types/openapi'; const BASE_URL = '/app-api/muse'; class ApiError extends Error { constructor(public code: string, message: string, public status: number) { super(message); } } export async function request( path: string, options: RequestInit = {} ): Promise { const response = await fetch(`${BASE_URL}${path}`, { headers: { 'Content-Type': 'application/json', 'X-API-Version': '1', ...options.headers, }, ...options, }); const body: Base.CommonResult = await response.json(); if (body.code !== 0) { throw new ApiError(String(body.code), body.msg, response.status); } return body.data as T; } export const api = { get: (path: string) => request(path), post: (path: string, data?: unknown) => request(path, { method: 'POST', body: data ? JSON.stringify(data) : undefined, }), put: (path: string, data?: unknown) => request(path, { method: 'PUT', body: data ? JSON.stringify(data) : undefined, }), delete: (path: string) => request(path, { method: 'DELETE' }), }; ``` - [ ] **Step 2: 复制类型包** ```bash cp docs/api-contracts/generated/typescript/*.ts muse-studio/src/types/ ``` - [ ] **Step 3: 编写类型入口** ```typescript // src/types/openapi.ts export type * as Base from './base'; export type * as Content from './content'; export type * as AI from './ai'; export type * as Knowledge from './knowledge'; export type * as Market from './market'; export type * as Account from './account'; export type * as Meta from './meta'; ``` - [ ] **Step 4: 提交** ```bash git add -A && git commit -m "feat(api): 集成 API 客户端 + OpenAPI 类型包" ``` ### Task 2.2: MSW Mock API 配置 **文件:** - 创建: `src/api/mocks/handlers/` 下各模块 handler - [ ] **Step 1: 安装 MSW** ```bash pnpm add -D msw@2 npx msw init public/ --save ``` - [ ] **Step 2: 编写 Content Mock Handlers** ```typescript // src/test/mocks/handlers/content.ts import { http, HttpResponse } from 'msw'; export const contentHandlers = [ // GET /app-api/muse/works — 作品列表 http.get('/app-api/muse/works', ({ request }) => { const url = new URL(request.url); const pageNo = parseInt(url.searchParams.get('pageNo') || '1'); return HttpResponse.json({ code: 0, msg: 'success', data: { total: 3, pageNo, pageSize: 20, list: [ { id: '1', title: '星海迷途', status: 'active', genre: '科幻', wordCount: 52300, chapterCount: 12, updatedAt: '2026-05-20T10:30:00Z', }, { id: '2', title: '长安旧事', status: 'draft', genre: '历史', wordCount: 12800, chapterCount: 3, updatedAt: '2026-05-24T08:00:00Z', }, { id: '3', title: '深渊笔记', status: 'active', genre: '悬疑', wordCount: 89500, chapterCount: 24, updatedAt: '2026-05-23T18:00:00Z', }, ], }, }); }), // POST /app-api/muse/works — 创建作品 http.post('/app-api/muse/works', async ({ request }) => { const body = await request.json(); const work = { id: crypto.randomUUID(), title: (body as any).title || '未命名作品', description: (body as any).description || '', status: 'draft', genre: (body as any).genre || '', wordCount: 0, chapterCount: 0, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; return HttpResponse.json({ code: 0, msg: 'success', data: work }, { status: 201 }); }), // GET /app-api/muse/works/:workId — 作品详情 http.get('/app-api/muse/works/:workId', ({ params }) => { return HttpResponse.json({ code: 0, msg: 'success', data: { id: params.workId, title: '星海迷途', description: '人类在星际探索中...', status: 'active', genre: '科幻', wordCount: 52300, chapterCount: 12, createdAt: '2026-05-01T00:00:00Z', updatedAt: '2026-05-20T10:30:00Z', }, }); }), ]; // src/api/mocks/handlers/ai.ts import { http, HttpResponse } from 'msw'; export const aiHandlers = [ // Mock AI SSE 流式生成接口 http.get('/app-api/muse/ai/stream', () => { const encoder = new TextEncoder(); const stream = new ReadableStream({ async start(controller) { const chunks = ['宇宙', '深处', '闪烁着', '未知的', '微光,', '那是', '文明的', '遗迹。']; for (const chunk of chunks) { await new Promise((resolve) => setTimeout(resolve, 100)); // 模拟 Token 渲染间隔 controller.enqueue( encoder.encode(`data: ${JSON.stringify({ type: 'chunk', content: chunk, sequenceNo: chunks.indexOf(chunk) })}\n\n`) ); } controller.enqueue( encoder.encode(`data: ${JSON.stringify({ type: 'done', generationId: 'g-mock-123', candidateId: 'c-mock-456' })}\n\n`) ); controller.close(); }, }); return new HttpResponse(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }, }); }), ]; ``` - [ ] **Step 3: 配置 MSW 入口** ```typescript // src/api/mocks/browser.ts import { setupWorker } from 'msw/browser'; import { contentHandlers } from './handlers/content'; import { aiHandlers } from './handlers/ai'; import { knowledgeHandlers } from './handlers/knowledge'; import { marketHandlers } from './handlers/market'; import { accountHandlers } from './handlers/account'; export const worker = setupWorker( ...contentHandlers, ...aiHandlers, ...knowledgeHandlers, ...marketHandlers, ...accountHandlers ); ``` - [ ] **Step 4: 在开发环境启用 MSW** ```tsx // src/main.tsx 开头追加 if (import.meta.env.DEV) { const { worker } = await import('./api/mocks/browser'); await worker.start({ onUnhandledRequest: 'bypass' }); } ``` - [ ] **Step 5: 提交** ```bash git add -A && git commit -m "feat(mock): 配置 MSW Mock API(Content handlers)" ``` ### Task 2.3: SSE 双通道客户端 **文件:** - 创建: `src/lib/sse.ts` ```typescript // src/lib/sse.ts /** * Muse SSE 双通道设计: * 1. AI stream: 独立连接,用于 AI 生成实时流 * 2. Event stream: 长连接,用于事件通知,支持 lastEventId 续传 */ export type SSEEventHandler = { onChunk?: (data: { content: string; sequenceNo: number }) => void; onQualityCheck?: (data: { dimension: string; score: number; passed: boolean }) => void; onDone?: (data: { generationId: string; candidateId: string }) => void; onError?: (data: { code: string; message: string }) => void; }; export function connectAIStream( url: string, handlers: SSEEventHandler ): AbortController { const controller = new AbortController(); fetch(url, { headers: { 'Accept': 'text/event-stream', 'X-API-Version': '1', }, signal: controller.signal, }).then(async (response) => { const reader = response.body!.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.startsWith('data: ')) { const data = JSON.parse(line.slice(6)); switch (data.type) { case 'chunk': handlers.onChunk?.(data); break; case 'quality_check': handlers.onQualityCheck?.(data); break; case 'done': handlers.onDone?.(data); break; case 'error': handlers.onError?.(data); break; } } } } }).catch((err) => { if (err.name !== 'AbortError') { handlers.onError?.({ code: 'SSE_ERROR', message: err.message }); } }); return controller; } export function connectEventStream( lastEventId?: string ): { close: () => void; on: (event: string, handler: (data: unknown) => void) => void } { const eventSource = new EventSource( `/app-api/muse/events${lastEventId ? `?lastEventId=${lastEventId}` : ''}` ); return { close: () => eventSource.close(), on: (event, handler) => { eventSource.addEventListener(event, (e) => { handler(JSON.parse(e.data)); }); }, }; } ``` - [ ] **Step: 提交** ```bash git add -A && git commit -m "feat(sse): 实现 SSE 双通道客户端(AI stream + Event stream)" ``` ### Task 2.4: IndexedDB 持久化层 **文件:** - 创建: `src/lib/indexed-db.ts` ```typescript // src/lib/indexed-db.ts const DB_NAME = 'muse-studio'; const BLOCK_STORE = 'block-drafts'; const DB_VERSION = 1; function openDB(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = () => { const db = request.result; if (!db.objectStoreNames.contains(BLOCK_STORE)) { db.createObjectStore(BLOCK_STORE, { keyPath: 'blockId' }); } }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } export async function saveBlockDraft(blockId: string, content: Record): Promise { const db = await openDB(); const tx = db.transaction(BLOCK_STORE, 'readwrite'); tx.objectStore(BLOCK_STORE).put({ blockId, content, savedAt: Date.now() }); } export async function getBlockDraft(blockId: string): Promise | null> { const db = await openDB(); const tx = db.transaction(BLOCK_STORE, 'readonly'); return new Promise((resolve, reject) => { const req = tx.objectStore(BLOCK_STORE).get(blockId); req.onsuccess = () => resolve(req.result?.content ?? null); req.onerror = () => reject(req.error); }); } export async function removeBlockDraft(blockId: string): Promise { const db = await openDB(); const tx = db.transaction(BLOCK_STORE, 'readwrite'); tx.objectStore(BLOCK_STORE).delete(blockId); } ``` - [ ] **Step: 提交** ```bash git add -A && git commit -m "feat(storage): 实现 IndexedDB 持久化层(Block draft 安全网)" ``` ### Task 2.5: Zustand 状态管理 **文件:** - 创建: `src/stores/editorStore.ts` - 创建: `src/stores/uiStore.ts` ```typescript // src/stores/editorStore.ts import { create } from 'zustand'; interface EditorState { activeBlockId: string | null; isDirty: boolean; lastSavedAt: Date | null; setActiveBlock: (blockId: string | null) => void; markDirty: () => void; markSaved: () => void; } export const useEditorStore = create((set) => ({ activeBlockId: null, isDirty: false, lastSavedAt: null, setActiveBlock: (blockId) => set({ activeBlockId: blockId, isDirty: false }), markDirty: () => set({ isDirty: true }), markSaved: () => set({ isDirty: false, lastSavedAt: new Date() }), })); ``` ```typescript // src/stores/uiStore.ts import { create } from 'zustand'; interface UIState { sidebarCollapsed: boolean; candidatePanelOpen: boolean; toggleSidebar: () => void; toggleCandidatePanel: () => void; } export const useUIStore = create((set) => ({ sidebarCollapsed: false, candidatePanelOpen: false, toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })), toggleCandidatePanel: () => set((s) => ({ candidatePanelOpen: !s.candidatePanelOpen })), })); ``` - [ ] **Step: 提交** ```bash git add -A && git commit -m "feat(state): 配置 Zustand store(编辑器状态 + UI 状态)" ``` --- ## Step 3: 功能域开发(按优先级) ### Feature 1: 写作台(核心路径) **优先级 1 — 编辑器集成 + AI 生成 + 候选面板** - [ ] **Task 3.1: Tiptap 编辑器集成** 文件: `src/features/editor/components/MuseEditor.tsx` ```tsx import { useEditor, EditorContent } from '@tiptap/react'; import StarterKit from '@tiptap/starter-kit'; import Placeholder from '@tiptap/extension-placeholder'; import { useEffect, useCallback } from 'react'; import { useEditorStore } from '@/stores/editorStore'; import { saveBlockDraft, getBlockDraft, removeBlockDraft } from '@/lib/indexed-db'; import { debounce } from '@/lib/debounce'; import { api } from '@/api/client'; interface MuseEditorProps { blockId: string; initialContent?: unknown; revision: number; } export default function MuseEditor({ blockId, initialContent, revision }: MuseEditorProps) { const { markDirty, markSaved } = useEditorStore(); const editor = useEditor({ extensions: [ StarterKit, Placeholder.configure({ placeholder: '开始创作...' }), ], content: initialContent as any, onUpdate: ({ editor }) => { markDirty(); // 自动保存到 IndexedDB(debounce 2s) debouncedSave(blockId, editor.getJSON()); }, autofocus: 'end', }); // 自动保存到后端 + IndexedDB 安全网(debounce 2s) const debouncedSave = useCallback( debounce(async (blockId: string, content: unknown) => { // 1. 先保存到 IndexedDB(即时,安全网) await saveBlockDraft(blockId, content as Record); // 2. 再调用 API(乐观锁) try { const result = await api.put<{ newRevision: number }>( `/app-api/muse/blocks/${blockId}`, { content: JSON.stringify(content), expectedRevision: revision } ); await removeBlockDraft(blockId); markSaved(); } catch (err) { // API 失败:IndexedDB 已保存,下次恢复 } }, 2000), [revision] ); // 恢复 IndexedDB 草稿 useEffect(() => { getBlockDraft(blockId).then((draft) => { if (draft !== null && draft !== undefined && editor) { editor.commands.setContent(draft as any); } }); }, [blockId, editor]); return (
); } ``` **关键共享模块:** `src/lib/debounce.ts` ```typescript // src/lib/debounce.ts — 通用去抖工具函数 export function debounce any>(fn: T, ms: number): T { let timer: ReturnType; return ((...args: any[]) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), ms); }) as T; } ``` - [ ] **Task 3.2: AI 生成面板** — 调用 AI SSE stream,展示实时流式输出 - [ ] **Task 3.3: 候选面板** — 展示候选列表,支持接受/拒绝,显示 diff - [ ] **Task 3.4: 编程式提交** ```bash git add -A && git commit -m "feat(editor): Tiptap 编辑器集成 + IndexedDB 安全网 + 自动保存" ``` ### Feature 2: 我的作品 + 工作台 **涉及的 API 端点:** `GET /works`、`POST /works`、`GET /works/:workId`、`DELETE /works/:workId`、`GET /works/:workId/chapters`、`POST /works/:workId/chapters`、`PATCH /works/:workId/chapters/:chapterId`、`DELETE /works/:workId/chapters/:chapterId` **文件:** - 创建: `src/features/editor/components/WorkListPage.tsx` - 创建: `src/features/editor/components/CreateWorkModal.tsx` - 创建: `src/features/editor/components/ChapterPanel.tsx` - 创建: `src/features/editor/hooks/useWorks.ts` **关键组件与 Props:** | 组件 | Props | 说明 | |------|-------|------| | WorkListPage | — | 作品列表页,使用 TanStack Query 拉取列表,展示卡片网格 | | CreateWorkModal | `open: boolean; onClose: () => void; onCreated: (work: WorkVO) => void` | 创建作品弹窗,React Hook Form + Zod 校验 | | ChapterPanel | `workId: string; chapters: ChapterVO[]; activeChapterId?: string; onSelect: (chapterId: string) => void; onDelete: (chapterId: string) => void` | 章节管理侧面板,支持拖拽排序 | **代码示例 — `useWorks` Hook:** ```typescript // src/features/editor/hooks/useWorks.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { api } from '@/api/client'; import type { WorkVO, WorkCreateDTO } from '@/types/openapi'; export function useWorks(pageNo = 1, pageSize = 20) { return useQuery({ queryKey: ['works', pageNo, pageSize], queryFn: () => api.get<{ total: number; list: WorkVO[] }>(`/works?pageNo=${pageNo}&pageSize=${pageSize}`), }); } export function useCreateWork() { const qc = useQueryClient(); return useMutation({ mutationFn: (dto: WorkCreateDTO) => api.post('/works', dto), onSuccess: () => { qc.invalidateQueries({ queryKey: ['works'] }); }, }); } export function useDeleteWork() { const qc = useQueryClient(); return useMutation({ mutationFn: (workId: string) => api.delete(`/works/${workId}`), onSuccess: () => { qc.invalidateQueries({ queryKey: ['works'] }); }, }); } ``` **测试场景:** 1. 列表页加载展示 3 个 Mock 作品卡片 2. 点击「创建作品」按钮,弹窗打开,填写表单,提交后列表刷新 3. 删除按钮点击后弹出确认框,确认后列表移除该条目 4. 点击作品卡片进入章节面板,展示章节列表 - [ ] **Task: 提交** ```bash git add -A && git commit -m "feat(workspace): 作品列表/创建/删除 + 章节管理面板" ``` --- ### Feature 3: 知识库工作台 **涉及的 API 端点:** `GET /knowledge/:workId/entities`、`POST /knowledge/:workId/entities`、`PATCH /knowledge/:workId/entities/:entityId`、`GET /knowledge/:workId/drafts`、`POST /knowledge/:workId/drafts/:draftId/confirm`、`POST /knowledge/:workId/drafts/:draftId/reject`、`GET /knowledge/:workId/graph` **文件:** - 创建: `src/features/knowledge/components/EntityList.tsx` - 创建: `src/features/knowledge/components/DraftPanel.tsx` - 创建: `src/features/knowledge/components/GraphStub.tsx` - 创建: `src/features/knowledge/hooks/useKnowledge.ts` **关键组件与 Props:** | 组件 | Props | 说明 | |------|-------|------| | EntityList | `workId: string` | 知识实体列表,支持按类型筛选、搜索关键字 | | DraftPanel | `workId: string; drafts: KnowledgeDraftVO[]; onConfirm: (draftId: string) => void; onReject: (draftId: string, reason: string) => void` | 知识草稿确认/拒绝面板,拒绝需填写原因 | | GraphStub | `workId: string; width?: number; height?: number` | 知识图谱可视化占位,后续接入 RAGFlow GraphRAG API | **代码示例 — `useKnowledgeDrafts` Hook:** ```typescript // src/features/knowledge/hooks/useKnowledge.ts import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { api } from '@/api/client'; import type { KnowledgeDraftVO, KnowledgeEntityVO } from '@/types/openapi'; export function useKnowledgeEntities(workId: string) { return useQuery({ queryKey: ['knowledge', workId, 'entities'], queryFn: () => api.get(`/knowledge/${workId}/entities`), enabled: !!workId, }); } export function useKnowledgeDrafts(workId: string) { return useQuery({ queryKey: ['knowledge', workId, 'drafts'], queryFn: () => api.get(`/knowledge/${workId}/drafts`), enabled: !!workId, }); } export function useConfirmDraft() { const qc = useQueryClient(); return useMutation({ mutationFn: ({ workId, draftId }: { workId: string; draftId: string }) => api.post(`/knowledge/${workId}/drafts/${draftId}/confirm`), onSuccess: (_, vars) => { qc.invalidateQueries({ queryKey: ['knowledge', vars.workId] }); }, }); } export function useRejectDraft() { const qc = useQueryClient(); return useMutation({ mutationFn: ({ workId, draftId, reason }: { workId: string; draftId: string; reason: string }) => api.post(`/knowledge/${workId}/drafts/${draftId}/reject`, { reason }), onSuccess: (_, vars) => { qc.invalidateQueries({ queryKey: ['knowledge', vars.workId] }); }, }); } ``` **测试场景:** 1. 实体列表加载展示,按类型(人物/地点/事件)筛选 2. 草稿面板展示待确认列表,点击「确认」更新状态 3. 点击「拒绝」,弹出拒绝原因输入框,提交后刷新 4. 知识图谱占位区域正确渲染,显示 "Graph visualization coming soon" 占位文本 - [ ] **Task: 提交** ```bash git add -A && git commit -m "feat(knowledge): 知识实体列表 + 草稿确认/拒绝面板 + 图谱占位" ``` --- ### Feature 4: 智能体工作台 **涉及的 API 端点:** `GET /agents`、`POST /agents`、`GET /agents/:agentId`、`PATCH /agents/:agentId`、`DELETE /agents/:agentId`、`GET /agents/:agentId/slots`、`PATCH /agents/:agentId/slots` **文件:** - 创建: `src/features/agent/components/AgentList.tsx` - 创建: `src/features/agent/components/AgentCreateForm.tsx` - 创建: `src/features/agent/components/SlotBindingPanel.tsx` - 创建: `src/features/agent/hooks/useAgents.ts` **关键组件与 Props:** | 组件 | Props | 说明 | |------|-------|------| | AgentList | `onSelect: (agentId: string) => void` | 智能体列表卡片,展示名称、模型、状态 | | AgentCreateForm | `onCreated: (agent: AgentVO) => void; onCancel: () => void` | 创建智能体表单:名称、system prompt、模型选择、温度等 | | SlotBindingPanel | `agentId: string; slots: AgentSlotVO[]; onBind: (slotId: string, toolId: string) => void` | 槽位-工具绑定面板,支持搜索可用工具 | **代码示例 — `AgentList` 组件:** ```tsx // src/features/agent/components/AgentList.tsx import { useAgents } from '../hooks/useAgents'; interface AgentListProps { onSelect: (agentId: string) => void; } export default function AgentList({ onSelect }: AgentListProps) { const { data, isLoading, error } = useAgents(); if (isLoading) return
加载中...
; if (error) return
加载失败
; return (
{data?.list.map((agent) => (
onSelect(agent.id)} >

{agent.name}

{agent.model}

{agent.status === 'active' ? '运行中' : '已停用'}
))}
); } ``` **测试场景:** 1. 智能体列表加载展示,每个卡片显示名称、模型、状态标签 2. 点击「创建智能体」打开表单,填写 system prompt、选择模型,提交后列表刷新 3. 点击卡片进入槽位绑定面板,为智能体绑定/解绑工具槽位 4. 删除智能体,确认后列表中移除 - [ ] **Task: 提交** ```bash git add -A && git commit -m "feat(agent): 智能体列表 + 创建表单 + 槽位绑定面板" ``` --- ### Feature 5: 市场 + 个人中心 **涉及的 API 端点:** `GET /market/assets`、`GET /market/assets/:assetId`、`POST /market/assets/:assetId/install`、`GET /account/profile`、`PATCH /account/profile`、`GET /account/usage-stats`、`GET /account/entitlements` **文件:** - 创建: `src/features/market/components/MarketBrowse.tsx` - 创建: `src/features/market/hooks/useMarket.ts` - 创建: `src/features/account/components/PersonalCenter.tsx` - 创建: `src/features/account/components/UsageStats.tsx` - 创建: `src/features/account/hooks/useAccount.ts` **关键组件与 Props:** | 组件 | Props | 说明 | |------|-------|------| | MarketBrowse | `onInstall: (assetId: string) => void` | 市场资源浏览页,支持按类型/标签筛选,展示资产卡片网格 | | PersonalCenter | — | 个人中心:头像、昵称、配额详情、使用统计概览 | | UsageStats | `stats: UsageStatsVO` | 用量统计面板:Token 消耗、存储用量、API 调用次数 | **代码示例 — `UsageStats` 组件:** ```tsx // src/features/account/components/UsageStats.tsx import type { UsageStatsVO } from '@/types/openapi'; interface UsageStatsProps { stats: UsageStatsVO; } export default function UsageStats({ stats }: UsageStatsProps) { const items: { label: string; value: string; used: number; total: number }[] = [ { label: 'Token 消耗', value: `${(stats.tokenUsed / 1000).toFixed(1)}K`, used: stats.tokenUsed, total: stats.tokenQuota }, { label: '存储用量', value: `${(stats.storageUsed / 1024 / 1024).toFixed(0)} MB`, used: stats.storageUsed, total: stats.storageQuota }, { label: 'API 调用', value: `${stats.apiCalls}`, used: stats.apiCalls, total: stats.apiQuota }, ]; return (
{items.map((item) => (
{item.label} {item.value} / {item.total > 0 ? item.total : '无限制'}
0 ? `${Math.min((item.used / item.total) * 100, 100)}%` : '0%' }} />
))}
); } ``` **测试场景:** 1. 市场页面加载展示资产卡片,筛选器切换有效 2. 点击「安装」按钮,调用安装 API,按钮变为「已安装」 3. 个人中心显示用户信息、配额用量进度条 4. 用量统计各指标数据正确渲染,百分比进度条与实际数据成比例 - [ ] **Task: 提交** ```bash git add -A && git commit -m "feat(market+account): 市场资源浏览 + 个人中心 + 用量统计" ``` --- ## 完成标准 - [ ] 6 个功能域页面可交互 - [ ] 组件单元测试覆盖率 ≥75%(Vitest + Testing Library) - [ ] Hook 测试覆盖率 ≥80% - [ ] E2E 测试覆盖核心写作流程(Playwright) - [ ] ESLint + Prettier + TypeScript 类型检查通过 - [ ] Vite build 成功