studio 缺 UI 端面补齐(用户选 Block 分割合并)。openapi export SplitBlock/MergeBlockRequest(Result 手写:OpenAPI 误声明 response content:never,后端注解缺失);useBlockStructure(useSplitBlock+useMergeBlock,onSuccess 失效 chapterDetail+chapterList);content mock split/merge handler(单 block 模型简化,校验乐观锁返回契约 block);useBlockStructure.test 3/3。 验证:vitest 3/3 + tsc 0 + eslint 0。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
621 lines
21 KiB
TypeScript
621 lines
21 KiB
TypeScript
import { http, HttpResponse } from 'msw';
|
||
|
||
/** 作品类型定义 */
|
||
export interface MockWork {
|
||
id: string;
|
||
title: string;
|
||
description: string;
|
||
status: string;
|
||
genre?: string;
|
||
workSchemaId?: number;
|
||
wordCount: number;
|
||
chapterCount: number;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}
|
||
|
||
/** 章节类型定义 */
|
||
export interface MockChapter {
|
||
id: string;
|
||
title: string;
|
||
wordCount: number;
|
||
sortOrder: number;
|
||
workId: string;
|
||
blockId: string;
|
||
blockVersion: number;
|
||
content: string;
|
||
}
|
||
|
||
interface WorkCreateRequest {
|
||
title?: string;
|
||
description?: string;
|
||
genre?: string;
|
||
schemaId?: number;
|
||
}
|
||
|
||
interface ChapterCreateRequest {
|
||
title?: string;
|
||
}
|
||
|
||
interface BlockSaveRequest {
|
||
content?: string;
|
||
expectedRevision?: number;
|
||
commandId?: string;
|
||
sourceSnapshot?: {
|
||
contentRevision?: number;
|
||
sourceType?: string;
|
||
sourceId?: string;
|
||
};
|
||
}
|
||
|
||
/** split/merge 结构编辑命令(mock 入参)。 */
|
||
interface BlockStructureRequest {
|
||
commandId?: string;
|
||
splitPosition?: number;
|
||
targetBlockId?: string;
|
||
expectedRevision?: number;
|
||
sourceSnapshot?: {
|
||
contentRevision?: number;
|
||
sourceType?: string;
|
||
sourceId?: string;
|
||
};
|
||
}
|
||
|
||
interface TiptapTextNode {
|
||
text?: string;
|
||
content?: TiptapTextNode[];
|
||
}
|
||
|
||
/** 从 Tiptap JSON 中递归提取文本节点,用于 Mock 层同步字数统计。 */
|
||
function collectTiptapText(node: unknown, textNodes: string[]): void {
|
||
if (typeof node !== 'object' || node === null) return;
|
||
|
||
const current = node as TiptapTextNode;
|
||
if (typeof current.text === 'string') {
|
||
textNodes.push(current.text);
|
||
}
|
||
if (Array.isArray(current.content)) {
|
||
current.content.forEach((child) => collectTiptapText(child, textNodes));
|
||
}
|
||
}
|
||
|
||
/** 为章节构造默认 Tiptap JSON 正文,保持 mock 数据与真实 Block 契约一致。 */
|
||
function createChapterContent(title: string, wordCount: number): string {
|
||
return JSON.stringify({
|
||
type: 'doc',
|
||
content: [
|
||
{
|
||
type: 'paragraph',
|
||
content: [
|
||
{
|
||
type: 'text',
|
||
text: `这是章节《${title}》的正文内容。当前已保存字数为 ${wordCount} 字。`,
|
||
},
|
||
],
|
||
},
|
||
],
|
||
});
|
||
}
|
||
|
||
/** 构造符合 Block 契约的 mock 对象(对齐 chapterDetail handler 的 block 结构),供 split/merge 返回。 */
|
||
function mockBlock(id: string, chapter: MockChapter, content: string, sortOrder: number, version: number) {
|
||
const textNodes: string[] = [];
|
||
try {
|
||
collectTiptapText(JSON.parse(content), textNodes);
|
||
} catch {
|
||
/* 正文非合法 JSON 时字数计 0,不阻断 mock。 */
|
||
}
|
||
return {
|
||
id,
|
||
workId: chapter.workId,
|
||
chapterId: chapter.id,
|
||
blockType: 'scene',
|
||
title: chapter.title,
|
||
content,
|
||
sortOrder,
|
||
version,
|
||
wordCount: textNodes.join('').length,
|
||
createdAt: '2026-05-20T00:00:00Z',
|
||
updatedAt: new Date().toISOString(),
|
||
};
|
||
}
|
||
|
||
/** 构造章节和首个正文 Block 的一体化 mock 数据。 */
|
||
function createMockChapter(
|
||
id: string,
|
||
title: string,
|
||
wordCount: number,
|
||
sortOrder: number,
|
||
workId: string
|
||
): MockChapter {
|
||
return {
|
||
id,
|
||
title,
|
||
wordCount,
|
||
sortOrder,
|
||
workId,
|
||
blockId: `block-${id}-main`,
|
||
blockVersion: 1,
|
||
content: createChapterContent(title, wordCount),
|
||
};
|
||
}
|
||
|
||
// 初始作品数据快照
|
||
const INITIAL_WORKS: MockWork[] = [
|
||
{
|
||
id: '1',
|
||
title: '星海迷途',
|
||
description: '人类在星际探索中遇到的未知文明与科技碰撞之旅...',
|
||
status: 'active',
|
||
genre: '科幻',
|
||
wordCount: 52300,
|
||
chapterCount: 3,
|
||
createdAt: '2026-05-01T00:00:00Z',
|
||
updatedAt: '2026-05-20T10:30:00Z',
|
||
},
|
||
{
|
||
id: '2',
|
||
title: '长安旧事',
|
||
description: '讲述大唐风雨飘摇中的平民故事。',
|
||
status: 'draft',
|
||
genre: '历史',
|
||
wordCount: 12800,
|
||
chapterCount: 2,
|
||
createdAt: '2026-05-10T00:00:00Z',
|
||
updatedAt: '2026-05-24T08:00:00Z',
|
||
},
|
||
{
|
||
id: '3',
|
||
title: '深渊笔记',
|
||
description: '极地深渊探险中的神秘发现。',
|
||
status: 'active',
|
||
genre: '悬疑',
|
||
wordCount: 89500,
|
||
chapterCount: 4,
|
||
createdAt: '2026-05-15T00:00:00Z',
|
||
updatedAt: '2026-05-23T18:00:00Z',
|
||
},
|
||
];
|
||
|
||
// 初始章节数据快照
|
||
const INITIAL_CHAPTERS: MockChapter[] = [
|
||
// 星海迷途章节
|
||
createMockChapter('1-1', '第一章 启程', 15000, 1, '1'),
|
||
createMockChapter('1-2', '第二章 信号', 18000, 2, '1'),
|
||
createMockChapter('1-3', '第三章 降落', 19300, 3, '1'),
|
||
// 长安旧事章节
|
||
createMockChapter('2-1', '第一章 惊蛰', 6000, 1, '2'),
|
||
createMockChapter('2-2', '第二章 春分', 6800, 2, '2'),
|
||
// 深渊笔记章节
|
||
createMockChapter('3-1', '第一章 迷雾', 20000, 1, '3'),
|
||
createMockChapter('3-2', '第二章 深潜', 22000, 2, '3'),
|
||
createMockChapter('3-3', '第三章 遗迹', 24000, 3, '3'),
|
||
createMockChapter('3-4', '第四章 归途', 23500, 4, '3'),
|
||
];
|
||
|
||
// 运行时的可变内存数据库状态
|
||
let mockWorks = JSON.parse(JSON.stringify(INITIAL_WORKS)) as MockWork[];
|
||
let mockChapters = JSON.parse(JSON.stringify(INITIAL_CHAPTERS)) as MockChapter[];
|
||
|
||
/** B5 planning section mock 类型 */
|
||
interface MockPlanningSection {
|
||
sectionKey: string;
|
||
sectionName: string;
|
||
content: Record<string, unknown>;
|
||
revision: number;
|
||
schemaVersion: number;
|
||
projectionVersion: number;
|
||
usedFieldKeys: string[];
|
||
}
|
||
|
||
/** Meta Schema 投影字段定义(固定 schema,供 meta-projection mock 按字段渲染表单) */
|
||
const PROJECTION_DEFS: Record<string, {
|
||
projectionKey: string; projectionName: string; schemaVersion: number; projectionVersion: number;
|
||
fields: Array<{ fieldKey: string; fieldName: string; fieldType: string; uiVisible: boolean; userEditable: boolean; enumValues?: string[] }>;
|
||
}> = {
|
||
setting: {
|
||
projectionKey: 'setting', projectionName: '故事设定', schemaVersion: 1, projectionVersion: 1,
|
||
fields: [
|
||
{ fieldKey: 'time_period', fieldName: '时代背景', fieldType: 'enum', uiVisible: true, userEditable: true, enumValues: ['古代', '现代', '未来'] },
|
||
{ fieldKey: 'world_overview', fieldName: '世界观概述', fieldType: 'text', uiVisible: true, userEditable: true },
|
||
],
|
||
},
|
||
};
|
||
|
||
let mockPlanningSections: Record<string, MockPlanningSection[]> = {};
|
||
|
||
/**
|
||
* 重置 Mock 内存数据库
|
||
* 在单元测试 beforeEach 中调用,保证测试套件之间的并发与环境完全隔离
|
||
*/
|
||
export function resetMockDb(): void {
|
||
mockWorks = JSON.parse(JSON.stringify(INITIAL_WORKS));
|
||
mockChapters = JSON.parse(JSON.stringify(INITIAL_CHAPTERS));
|
||
mockPlanningSections = {};
|
||
}
|
||
|
||
/**
|
||
* 作品与内容管理 API 的 MSW Mock 处理器
|
||
*/
|
||
export const contentHandlers = [
|
||
// ===== B5 planning 编辑器:planning 结构 + meta-projection 字段 =====
|
||
// GET planning 结构(已确认的 sections)
|
||
http.get('/app-api/muse/works/:workId/planning', ({ params }) => {
|
||
const sections = mockPlanningSections[String(params.workId)] ?? [];
|
||
return HttpResponse.json({ code: 0, msg: 'success', data: {
|
||
workId: String(params.workId),
|
||
revision: sections.reduce((m, s) => Math.max(m, s.revision), 0),
|
||
sections,
|
||
} });
|
||
}),
|
||
// GET meta-projection 摘要列表
|
||
http.get('/app-api/muse/works/:workId/meta-projections', () => {
|
||
const list = Object.values(PROJECTION_DEFS).map((p) => ({
|
||
projectionKey: p.projectionKey, projectionName: p.projectionName,
|
||
schemaVersion: p.schemaVersion, projectionVersion: p.projectionVersion, fieldCount: p.fields.length,
|
||
}));
|
||
return HttpResponse.json({ code: 0, msg: 'success', data: list });
|
||
}),
|
||
// GET meta-projection 详情(fields[].value 回填已存 planning 值)
|
||
http.get('/app-api/muse/works/:workId/meta-projections/:projectionKey', ({ params }) => {
|
||
const def = PROJECTION_DEFS[String(params.projectionKey)];
|
||
if (!def) return HttpResponse.json({ code: 404, msg: 'projection 不存在', data: null });
|
||
const sections = mockPlanningSections[String(params.workId)] ?? [];
|
||
const saved = sections.find((s) => s.sectionKey === def.projectionKey);
|
||
const fields = def.fields.map((f) => ({ ...f, value: saved?.content[f.fieldKey] }));
|
||
return HttpResponse.json({ code: 0, msg: 'success', data: {
|
||
projectionKey: def.projectionKey, projectionName: def.projectionName,
|
||
schemaVersion: def.schemaVersion, projectionVersion: def.projectionVersion,
|
||
dataRevision: saved?.revision ?? 0, fields,
|
||
} });
|
||
}),
|
||
// PUT 保存 planning 项(记录 content + usedFieldKeys,revision 自增)
|
||
http.put('/app-api/muse/works/:workId/planning/:sectionKey', async ({ params, request }) => {
|
||
const body = (await request.json()) as { content: Record<string, unknown>; usedFieldKeys?: string[] };
|
||
const workId = String(params.workId);
|
||
const sectionKey = String(params.sectionKey);
|
||
const sections = mockPlanningSections[workId] ?? (mockPlanningSections[workId] = []);
|
||
const def = PROJECTION_DEFS[sectionKey];
|
||
let section = sections.find((s) => s.sectionKey === sectionKey);
|
||
if (!section) {
|
||
section = {
|
||
sectionKey, sectionName: def?.projectionName ?? sectionKey, content: {},
|
||
revision: 0, schemaVersion: def?.schemaVersion ?? 1, projectionVersion: def?.projectionVersion ?? 1,
|
||
usedFieldKeys: [],
|
||
};
|
||
sections.push(section);
|
||
}
|
||
section.content = body.content;
|
||
section.usedFieldKeys = body.usedFieldKeys ?? [];
|
||
section.revision += 1;
|
||
return HttpResponse.json({ code: 0, msg: 'success', data: {
|
||
sectionKey, newRevision: section.revision, updatedAt: '2026-06-21T00:00:00Z',
|
||
} });
|
||
}),
|
||
// POST 校验动态字段(mock 简化:全 valid)
|
||
http.post('/app-api/muse/works/:workId/dynamic-fields/validate', async ({ request }) => {
|
||
const body = (await request.json()) as { fields: Array<{ fieldKey: string }> };
|
||
return HttpResponse.json({ code: 0, msg: 'success', data: {
|
||
valid: true, fieldResults: body.fields.map((f) => ({ fieldKey: f.fieldKey, valid: true })),
|
||
} });
|
||
}),
|
||
|
||
// GET /app-api/muse/works — 获取作品列表
|
||
http.get('/app-api/muse/works', () => {
|
||
return HttpResponse.json({
|
||
code: 0,
|
||
msg: 'success',
|
||
data: {
|
||
total: mockWorks.length,
|
||
pageNo: 1,
|
||
pageSize: 20,
|
||
list: mockWorks,
|
||
},
|
||
});
|
||
}),
|
||
|
||
// POST /app-api/muse/works — 创建作品
|
||
http.post('/app-api/muse/works', async ({ request }) => {
|
||
const body = (await request.json()) as WorkCreateRequest;
|
||
|
||
// 严格字段解析与空安全守卫
|
||
const work: MockWork = {
|
||
id: crypto.randomUUID(),
|
||
title: body.title || '未命名作品',
|
||
description: body.description || '',
|
||
status: 'draft',
|
||
wordCount: 0,
|
||
chapterCount: 0,
|
||
createdAt: new Date().toISOString(),
|
||
updatedAt: new Date().toISOString(),
|
||
};
|
||
if (body.genre !== undefined) {
|
||
work.genre = body.genre;
|
||
}
|
||
if (body.schemaId !== undefined) {
|
||
work.workSchemaId = body.schemaId;
|
||
}
|
||
|
||
mockWorks.push(work);
|
||
|
||
return HttpResponse.json({ code: 0, msg: 'success', data: work }, { status: 201 });
|
||
}),
|
||
|
||
// GET /app-api/muse/works/schema-options — 作品可绑定的 Meta Schema 选项(置 :workId 之前,避免被参数路由吞掉)
|
||
http.get('/app-api/muse/works/schema-options', () => {
|
||
return HttpResponse.json({
|
||
code: 0,
|
||
msg: 'success',
|
||
data: [
|
||
{ id: 1, schemaKey: 'work_core', displayName: '作品核心结构', targetType: 'work' },
|
||
{ id: 2, schemaKey: 'work_outline', displayName: '大纲结构', targetType: 'work' },
|
||
],
|
||
});
|
||
}),
|
||
|
||
// GET /app-api/muse/works/:workId — 获取作品详情
|
||
http.get('/app-api/muse/works/:workId', ({ params }) => {
|
||
const { workId } = params;
|
||
const work = mockWorks.find((w) => w.id === workId);
|
||
|
||
if (!work) {
|
||
return HttpResponse.json({ code: 40401, msg: '作品不存在', data: null }, { status: 404 });
|
||
}
|
||
|
||
return HttpResponse.json({
|
||
code: 0,
|
||
msg: 'success',
|
||
data: work,
|
||
});
|
||
}),
|
||
|
||
// DELETE /app-api/muse/works/:workId — 删除作品 (包含关联章节的级联删除)
|
||
http.delete('/app-api/muse/works/:workId', ({ params }) => {
|
||
const { workId } = params;
|
||
const workIndex = mockWorks.findIndex((w) => w.id === workId);
|
||
|
||
if (workIndex === -1) {
|
||
return HttpResponse.json({ code: 40401, msg: '作品不存在', data: null }, { status: 404 });
|
||
}
|
||
|
||
// 移出作品
|
||
mockWorks.splice(workIndex, 1);
|
||
// 级联删除所属该作品的所有章节
|
||
mockChapters = mockChapters.filter((ch) => ch.workId !== workId);
|
||
|
||
return HttpResponse.json({
|
||
code: 0,
|
||
msg: 'success',
|
||
data: true,
|
||
});
|
||
}),
|
||
|
||
// GET /app-api/muse/works/:workId/chapters — 获取特定作品的章节列表 (按 sortOrder 升序)
|
||
http.get('/app-api/muse/works/:workId/chapters', ({ params }) => {
|
||
const { workId } = params;
|
||
|
||
// 过滤出该作品下的所有章节并排序
|
||
const list = mockChapters
|
||
.filter((ch) => ch.workId === workId)
|
||
.sort((a, b) => a.sortOrder - b.sortOrder);
|
||
|
||
return HttpResponse.json({
|
||
code: 0,
|
||
msg: 'success',
|
||
data: list,
|
||
});
|
||
}),
|
||
|
||
// GET /app-api/muse/works/:workId/chapters/:chapterId — 获取章节详情及 Block 正文
|
||
http.get('/app-api/muse/works/:workId/chapters/:chapterId', ({ params }) => {
|
||
const { workId, chapterId } = params;
|
||
const chapter = mockChapters.find((ch) => ch.id === chapterId && ch.workId === workId);
|
||
|
||
if (!chapter) {
|
||
return HttpResponse.json({ code: 40402, msg: '章节不存在', data: null }, { status: 404 });
|
||
}
|
||
|
||
return HttpResponse.json({
|
||
code: 0,
|
||
msg: 'success',
|
||
data: {
|
||
id: chapter.id,
|
||
workId: chapter.workId,
|
||
title: chapter.title,
|
||
sortOrder: chapter.sortOrder,
|
||
status: 'draft',
|
||
wordCount: chapter.wordCount,
|
||
blockCount: 1,
|
||
createdAt: '2026-05-20T00:00:00Z',
|
||
updatedAt: new Date().toISOString(),
|
||
blocks: [
|
||
{
|
||
id: chapter.blockId,
|
||
workId: chapter.workId,
|
||
chapterId: chapter.id,
|
||
blockType: 'scene',
|
||
title: chapter.title,
|
||
content: chapter.content,
|
||
sortOrder: 1,
|
||
version: chapter.blockVersion,
|
||
wordCount: chapter.wordCount,
|
||
createdAt: '2026-05-20T00:00:00Z',
|
||
updatedAt: new Date().toISOString(),
|
||
},
|
||
],
|
||
},
|
||
});
|
||
}),
|
||
|
||
// POST /app-api/muse/works/:workId/chapters — 创建章节
|
||
http.post('/app-api/muse/works/:workId/chapters', async ({ params, request }) => {
|
||
const { workId } = params;
|
||
const body = (await request.json()) as ChapterCreateRequest;
|
||
|
||
const list = mockChapters.filter((ch) => ch.workId === workId);
|
||
|
||
const newChapter: MockChapter = {
|
||
id: crypto.randomUUID(),
|
||
title: body.title || `第 ${list.length + 1} 章 未命名章节`,
|
||
wordCount: 0,
|
||
sortOrder: list.length + 1,
|
||
workId: workId as string,
|
||
blockId: crypto.randomUUID(),
|
||
blockVersion: 1,
|
||
content: createChapterContent(body.title || `第 ${list.length + 1} 章 未命名章节`, 0),
|
||
};
|
||
|
||
mockChapters.push(newChapter);
|
||
|
||
// 级联更新对应作品的 chapterCount
|
||
const work = mockWorks.find((w) => w.id === workId);
|
||
if (work) {
|
||
work.chapterCount += 1;
|
||
work.updatedAt = new Date().toISOString();
|
||
}
|
||
|
||
return HttpResponse.json({
|
||
code: 0,
|
||
msg: 'success',
|
||
data: newChapter,
|
||
}, { status: 201 });
|
||
}),
|
||
|
||
// DELETE /app-api/muse/works/:workId/chapters/:chapterId — 删除小节/章节
|
||
http.delete('/app-api/muse/works/:workId/chapters/:chapterId', ({ params }) => {
|
||
const { workId, chapterId } = params;
|
||
const chapterIndex = mockChapters.findIndex((ch) => ch.id === chapterId && ch.workId === workId);
|
||
|
||
if (chapterIndex === -1) {
|
||
return HttpResponse.json({ code: 40402, msg: '章节不存在', data: null }, { status: 404 });
|
||
}
|
||
|
||
const removedChapter = mockChapters[chapterIndex];
|
||
const removedWords = removedChapter ? removedChapter.wordCount : 0;
|
||
|
||
// 移出章节
|
||
mockChapters.splice(chapterIndex, 1);
|
||
|
||
// 级联更新对应作品的 chapterCount 与 wordCount
|
||
const work = mockWorks.find((w) => w.id === workId);
|
||
if (work) {
|
||
work.chapterCount = Math.max(0, work.chapterCount - 1);
|
||
work.wordCount = Math.max(0, work.wordCount - removedWords);
|
||
work.updatedAt = new Date().toISOString();
|
||
}
|
||
|
||
return HttpResponse.json({
|
||
code: 0,
|
||
msg: 'success',
|
||
data: true,
|
||
});
|
||
}),
|
||
|
||
// PUT /app-api/muse/works/:workId/blocks/:blockId — 编辑器保存 Block 正文接口拦截
|
||
http.put('/app-api/muse/works/:workId/blocks/:blockId', async ({ params, request }) => {
|
||
const { workId, blockId } = params;
|
||
const body = (await request.json()) as BlockSaveRequest;
|
||
const chapter = mockChapters.find((ch) => ch.blockId === blockId && ch.workId === workId);
|
||
|
||
if (!chapter) {
|
||
return HttpResponse.json({ code: 40403, msg: 'Block 不存在', data: null }, { status: 404 });
|
||
}
|
||
if (!body.commandId || !body.sourceSnapshot) {
|
||
return HttpResponse.json({ code: 40001, msg: '缺少保存命令字段', data: null }, { status: 400 });
|
||
}
|
||
if (body.expectedRevision !== chapter.blockVersion) {
|
||
return HttpResponse.json({ code: 40901, msg: 'Block 版本已变化,请刷新后重试', data: null }, { status: 409 });
|
||
}
|
||
|
||
// 获取提交的正文 JSON 以计算字数进行 Mock 存储更新
|
||
let newWordCount = 0;
|
||
try {
|
||
if (body.content) {
|
||
const parsed: unknown = JSON.parse(body.content);
|
||
// 简易字数计算:遍历文本节点节点获取字符总长
|
||
const textNodes: string[] = [];
|
||
collectTiptapText(parsed, textNodes);
|
||
newWordCount = textNodes.join('').length;
|
||
}
|
||
} catch {
|
||
newWordCount = 100; // 降级默认字数
|
||
}
|
||
|
||
// 查找到该章节并更新其字数、正文和乐观锁版本
|
||
const oldWords = chapter.wordCount;
|
||
chapter.wordCount = newWordCount;
|
||
chapter.content = body.content || chapter.content;
|
||
chapter.blockVersion += 1;
|
||
|
||
// 联动更新关联作品的总字数统计
|
||
const work = mockWorks.find((w) => w.id === chapter.workId);
|
||
if (work) {
|
||
work.wordCount = Math.max(0, work.wordCount - oldWords + newWordCount);
|
||
work.updatedAt = new Date().toISOString();
|
||
}
|
||
|
||
return HttpResponse.json({
|
||
code: 0,
|
||
msg: 'success',
|
||
data: {
|
||
newVersion: chapter.blockVersion,
|
||
},
|
||
});
|
||
}),
|
||
|
||
// POST /app-api/muse/works/:workId/blocks/:blockId/split — 分割 Block(mock:校验乐观锁后返回原+新两个 Block)
|
||
http.post('/app-api/muse/works/:workId/blocks/:blockId/split', async ({ params, request }) => {
|
||
const { workId, blockId } = params;
|
||
const body = (await request.json()) as BlockStructureRequest;
|
||
const chapter = mockChapters.find((ch) => ch.blockId === blockId && ch.workId === workId);
|
||
|
||
if (!chapter) {
|
||
return HttpResponse.json({ code: 40403, msg: 'Block 不存在', data: null }, { status: 404 });
|
||
}
|
||
if (!body.commandId || !body.sourceSnapshot) {
|
||
return HttpResponse.json({ code: 40001, msg: '缺少分割命令字段', data: null }, { status: 400 });
|
||
}
|
||
if (body.expectedRevision !== chapter.blockVersion) {
|
||
return HttpResponse.json({ code: 40901, msg: 'Block 版本已变化,请刷新后重试', data: null }, { status: 409 });
|
||
}
|
||
|
||
// mock 不持久化多 Block(单 Block 模型),仅校验后返回符合契约的两个 Block;真实多 Block 由真后端 e2e 覆盖。
|
||
chapter.blockVersion += 1;
|
||
const newBlockId = crypto.randomUUID();
|
||
return HttpResponse.json({
|
||
code: 0,
|
||
msg: 'success',
|
||
data: {
|
||
originalBlock: mockBlock(chapter.blockId, chapter, chapter.content, 1, chapter.blockVersion),
|
||
newBlock: mockBlock(newBlockId, chapter, createChapterContent(chapter.title, 0), 2, 1),
|
||
},
|
||
});
|
||
}),
|
||
|
||
// POST /app-api/muse/works/:workId/blocks/:blockId/merge — 合并 Block(mock:校验乐观锁后返回合并后 Block)
|
||
http.post('/app-api/muse/works/:workId/blocks/:blockId/merge', async ({ params, request }) => {
|
||
const { workId, blockId } = params;
|
||
const body = (await request.json()) as BlockStructureRequest;
|
||
const chapter = mockChapters.find((ch) => ch.blockId === blockId && ch.workId === workId);
|
||
|
||
if (!chapter) {
|
||
return HttpResponse.json({ code: 40403, msg: 'Block 不存在', data: null }, { status: 404 });
|
||
}
|
||
if (!body.commandId || !body.sourceSnapshot) {
|
||
return HttpResponse.json({ code: 40001, msg: '缺少合并命令字段', data: null }, { status: 400 });
|
||
}
|
||
if (body.expectedRevision !== chapter.blockVersion) {
|
||
return HttpResponse.json({ code: 40901, msg: 'Block 版本已变化,请刷新后重试', data: null }, { status: 409 });
|
||
}
|
||
|
||
chapter.blockVersion += 1;
|
||
return HttpResponse.json({
|
||
code: 0,
|
||
msg: 'success',
|
||
data: {
|
||
mergedBlock: mockBlock(chapter.blockId, chapter, chapter.content, 1, chapter.blockVersion),
|
||
},
|
||
});
|
||
}),
|
||
];
|