# 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 成功