|
|
@@ -1,229 +1,353 @@
|
|
|
-import { NextRequest, NextResponse } from 'next/server';
|
|
|
-import { promises as fs } from 'fs';
|
|
|
-import path from 'path';
|
|
|
-import { NovelType, DEFAULT_WORLD_STATE_FILES, DEFAULT_LORE_FILES } from '@/app/lib/novel-types';
|
|
|
-import { getDefaultTemplate, getNovelTypeTemplate } from '@/app/lib/novel-utils';
|
|
|
-import { isPathSafe } from '@/app/lib/novel-server-utils';
|
|
|
-
|
|
|
-interface InitRequest {
|
|
|
- projectPath: string;
|
|
|
- novelType: NovelType;
|
|
|
- worldDescription?: string;
|
|
|
- apiSettings?: {
|
|
|
- apiUrl: string;
|
|
|
- apiKey: string;
|
|
|
- model: string;
|
|
|
- };
|
|
|
- writingStylePreset?: string;
|
|
|
- writingStyleDescription?: string;
|
|
|
- filesToGenerate?: string[];
|
|
|
- /** 需求 1.11:用户在向导中选择禁用的 World_State_File 文件名列表 */
|
|
|
- disabledFiles?: string[];
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 使用 AI 为指定文件生成内容
|
|
|
- */
|
|
|
-async function generateWithAI(
|
|
|
- fileName: string,
|
|
|
- worldDescription: string,
|
|
|
- novelType: NovelType,
|
|
|
- apiSettings: { apiUrl: string; apiKey: string; model: string },
|
|
|
- writingStylePreset?: string,
|
|
|
- writingStyleDescription?: string
|
|
|
-): Promise<string> {
|
|
|
- let stylePrompt = '';
|
|
|
- if (writingStylePreset || writingStyleDescription) {
|
|
|
- stylePrompt = '\n\n文风设定:';
|
|
|
- if (writingStylePreset) {
|
|
|
- stylePrompt += `\n- 预设文风:${writingStylePreset}`;
|
|
|
- }
|
|
|
- if (writingStyleDescription) {
|
|
|
- stylePrompt += `\n- 风格描述:${writingStyleDescription}`;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const prompt = `你是一位专业的小说文风设计师。请根据以下信息,为小说生成详细的文风档案(writing_style.md)。
|
|
|
-
|
|
|
-世界观描述:${worldDescription}
|
|
|
-小说类型:${novelType}${stylePrompt}
|
|
|
-
|
|
|
-请生成一份完整的文风档案,包括:
|
|
|
-1. 整体风格定位
|
|
|
-2. 叙事视角选择
|
|
|
-3. 语言特点(句式、用词、描写侧重)
|
|
|
-4. 节奏控制
|
|
|
-5. 禁忌与避免
|
|
|
-
|
|
|
-格式为 Markdown,内容要具体、详细、有创意。`;
|
|
|
-
|
|
|
- const response = await fetch(apiSettings.apiUrl, {
|
|
|
- method: 'POST',
|
|
|
- headers: {
|
|
|
- 'Content-Type': 'application/json',
|
|
|
- Authorization: `Bearer ${apiSettings.apiKey}`,
|
|
|
- },
|
|
|
- body: JSON.stringify({
|
|
|
- model: apiSettings.model,
|
|
|
- messages: [{ role: 'user', content: prompt }],
|
|
|
- max_tokens: 2000,
|
|
|
- }),
|
|
|
- signal: AbortSignal.timeout(60000),
|
|
|
- });
|
|
|
-
|
|
|
- if (!response.ok) {
|
|
|
- throw new Error(`AI request failed with status ${response.status}`);
|
|
|
- }
|
|
|
-
|
|
|
- const data = await response.json();
|
|
|
- const content = data.choices?.[0]?.message?.content;
|
|
|
-
|
|
|
- if (!content || typeof content !== 'string' || content.trim() === '') {
|
|
|
- throw new Error('AI returned empty content');
|
|
|
- }
|
|
|
-
|
|
|
- return content.trim();
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 原子写入文件(先写临时文件,再重命名)
|
|
|
- */
|
|
|
-async function atomicWrite(filePath: string, content: string): Promise<void> {
|
|
|
- const tmpPath = `${filePath}.tmp`;
|
|
|
- await fs.writeFile(tmpPath, content, 'utf-8');
|
|
|
- await fs.rename(tmpPath, filePath);
|
|
|
-}
|
|
|
-
|
|
|
-export async function POST(request: NextRequest) {
|
|
|
- let body: InitRequest;
|
|
|
-
|
|
|
- try {
|
|
|
- body = await request.json();
|
|
|
- } catch {
|
|
|
- return NextResponse.json({ success: false, error: '无效的请求体' }, { status: 400 });
|
|
|
- }
|
|
|
-
|
|
|
- const { projectPath, novelType, worldDescription, apiSettings, disabledFiles = [] } = body;
|
|
|
-
|
|
|
- // 1. 路径安全验证
|
|
|
- if (!projectPath || !isPathSafe(projectPath)) {
|
|
|
- return NextResponse.json({ success: false, error: '无效的项目路径' }, { status: 400 });
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- // 2. 创建目录结构
|
|
|
- const dirs = [
|
|
|
- path.join(projectPath, 'world_state'),
|
|
|
- path.join(projectPath, 'world_state', 'lore'),
|
|
|
- path.join(projectPath, 'chapters'),
|
|
|
- path.join(projectPath, 'control-cards'),
|
|
|
- path.join(projectPath, 'logs'),
|
|
|
- path.join(projectPath, 'history'),
|
|
|
- ];
|
|
|
-
|
|
|
- for (const dir of dirs) {
|
|
|
- await fs.mkdir(dir, { recursive: true });
|
|
|
- }
|
|
|
-
|
|
|
- // 需求 1.10:获取按 Novel_Type 差异化的模板内容
|
|
|
- const novelTypeTemplates = getNovelTypeTemplate(novelType);
|
|
|
-
|
|
|
- // 3. 准备所有要写入的文件(一次性收集)
|
|
|
- const filesToWrite: Array<{ filePath: string; content: string }> = [];
|
|
|
-
|
|
|
- // 3.1 World_State_File 模板(跳过用户禁用的文件)
|
|
|
- for (const fileName of DEFAULT_WORLD_STATE_FILES) {
|
|
|
- if (disabledFiles.includes(fileName)) continue;
|
|
|
- const filePath = path.join(projectPath, 'world_state', fileName);
|
|
|
- const template = novelTypeTemplates[fileName] ?? getDefaultTemplate(fileName);
|
|
|
- filesToWrite.push({ filePath, content: template });
|
|
|
- }
|
|
|
-
|
|
|
- // 3.2 World_Lore_File 模板(跳过用户禁用的文件)
|
|
|
- for (const fileName of DEFAULT_LORE_FILES) {
|
|
|
- if (disabledFiles.includes(fileName)) continue;
|
|
|
- const filePath = path.join(projectPath, 'world_state', 'lore', fileName);
|
|
|
- const template = getDefaultTemplate(fileName);
|
|
|
- filesToWrite.push({ filePath, content: template });
|
|
|
- }
|
|
|
-
|
|
|
- // 3.3 AI 生成内容(如果提供了 apiSettings 和 worldDescription)
|
|
|
- const generatedFiles: string[] = [];
|
|
|
- const failedFiles: string[] = [];
|
|
|
-
|
|
|
- if (apiSettings && worldDescription) {
|
|
|
- // 只生成文风设定(writing_style.md),世界观和世界状态使用默认模板
|
|
|
- const generateWritingStyle = !disabledFiles.includes('writing_style.md');
|
|
|
-
|
|
|
- if (generateWritingStyle) {
|
|
|
- const filePath = path.join(projectPath, 'world_state', 'writing_style.md');
|
|
|
-
|
|
|
- try {
|
|
|
- const content = await generateWithAI(
|
|
|
- 'writing_style.md',
|
|
|
- worldDescription,
|
|
|
- novelType,
|
|
|
- apiSettings,
|
|
|
- body.writingStylePreset,
|
|
|
- body.writingStyleDescription
|
|
|
- );
|
|
|
- // 更新 filesToWrite 中的对应文件内容
|
|
|
- const existingIndex = filesToWrite.findIndex((f) => f.filePath === filePath);
|
|
|
- if (existingIndex >= 0) {
|
|
|
- filesToWrite[existingIndex].content = content;
|
|
|
- }
|
|
|
- generatedFiles.push('writing_style.md');
|
|
|
- } catch {
|
|
|
- // 失败则保留模板内容(错误隔离)
|
|
|
- failedFiles.push('writing_style.md');
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 3.4 novel_project.json(如果不存在)
|
|
|
- const projectConfigPath = path.join(projectPath, 'novel_project.json');
|
|
|
- let configExists = false;
|
|
|
- try {
|
|
|
- await fs.access(projectConfigPath);
|
|
|
- configExists = true;
|
|
|
- } catch {
|
|
|
- configExists = false;
|
|
|
- }
|
|
|
-
|
|
|
- if (!configExists) {
|
|
|
- const disabledWorldStateFiles = disabledFiles.filter((f) =>
|
|
|
- (DEFAULT_WORLD_STATE_FILES as readonly string[]).includes(f)
|
|
|
- );
|
|
|
-
|
|
|
- const projectConfig = {
|
|
|
- name: path.basename(projectPath),
|
|
|
- novelType,
|
|
|
- createdAt: new Date().toISOString(),
|
|
|
- lastOpenedAt: new Date().toISOString(),
|
|
|
- chapterWordCountMin: 2000,
|
|
|
- chapterWordCountMax: 4000,
|
|
|
- recentChaptersCount: 3,
|
|
|
- disabledWorldStateFiles,
|
|
|
- writingConstraints: [],
|
|
|
- antiHomogenizationStrategies: [],
|
|
|
- lastUsedPromptTemplate: '',
|
|
|
- };
|
|
|
- filesToWrite.push({ filePath: projectConfigPath, content: JSON.stringify(projectConfig, null, 2) });
|
|
|
- }
|
|
|
-
|
|
|
- // 4. 一次性写入所有文件
|
|
|
- for (const { filePath, content } of filesToWrite) {
|
|
|
- await atomicWrite(filePath, content);
|
|
|
- }
|
|
|
-
|
|
|
- // 5. 返回结果
|
|
|
- return NextResponse.json({
|
|
|
- success: true,
|
|
|
- generatedFiles,
|
|
|
- failedFiles,
|
|
|
- });
|
|
|
- } catch (error) {
|
|
|
- const message = error instanceof Error ? error.message : '初始化失败';
|
|
|
- return NextResponse.json({ success: false, error: message }, { status: 500 });
|
|
|
- }
|
|
|
-}
|
|
|
+import { NextRequest, NextResponse } from 'next/server';
|
|
|
+import { promises as fs } from 'fs';
|
|
|
+import path from 'path';
|
|
|
+import { NovelType, DEFAULT_WORLD_STATE_FILES, DEFAULT_LORE_FILES } from '@/app/lib/novel-types';
|
|
|
+import { getDefaultTemplate, getNovelTypeTemplate } from '@/app/lib/novel-utils';
|
|
|
+import { isPathSafe } from '@/app/lib/novel-server-utils';
|
|
|
+
|
|
|
+interface InitRequest {
|
|
|
+ projectPath: string;
|
|
|
+ novelType: NovelType;
|
|
|
+ worldDescription?: string;
|
|
|
+ apiSettings?: {
|
|
|
+ apiUrl: string;
|
|
|
+ apiKey: string;
|
|
|
+ model: string;
|
|
|
+ };
|
|
|
+ writingStylePreset?: string;
|
|
|
+ writingStyleDescription?: string;
|
|
|
+ filesToGenerate?: string[];
|
|
|
+ /** 需求 1.11:用户在向导中选择禁用的 World_State_File 文件名列表 */
|
|
|
+ disabledFiles?: string[];
|
|
|
+ generateIronRules?: boolean;
|
|
|
+ generateStyleAnchor?: boolean;
|
|
|
+ styleInfo?: {
|
|
|
+ primaryStyle: string;
|
|
|
+ supportStyle?: string;
|
|
|
+ intensity: number;
|
|
|
+ styleDescription: string;
|
|
|
+ paragraphMode: string;
|
|
|
+ chapterTitles: boolean;
|
|
|
+ novelTypeName: string;
|
|
|
+ };
|
|
|
+ interviewData?: {
|
|
|
+ characters: string;
|
|
|
+ plotDirection: string;
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 使用 AI 为指定文件生成内容
|
|
|
+ */
|
|
|
+async function generateWithAI(
|
|
|
+ fileName: string,
|
|
|
+ worldDescription: string,
|
|
|
+ novelType: NovelType,
|
|
|
+ apiSettings: { apiUrl: string; apiKey: string; model: string },
|
|
|
+ writingStylePreset?: string,
|
|
|
+ writingStyleDescription?: string
|
|
|
+): Promise<string> {
|
|
|
+ let stylePrompt = '';
|
|
|
+ if (writingStylePreset || writingStyleDescription) {
|
|
|
+ stylePrompt = '\n\n文风设定:';
|
|
|
+ if (writingStylePreset) {
|
|
|
+ stylePrompt += `\n- 预设文风:${writingStylePreset}`;
|
|
|
+ }
|
|
|
+ if (writingStyleDescription) {
|
|
|
+ stylePrompt += `\n- 风格描述:${writingStyleDescription}`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const prompt = `你是一位专业的小说文风设计师。请根据以下信息,为小说生成详细的文风档案(writing_style.md)。
|
|
|
+
|
|
|
+世界观描述:${worldDescription}
|
|
|
+小说类型:${novelType}${stylePrompt}
|
|
|
+
|
|
|
+请生成一份完整的文风档案,包括:
|
|
|
+1. 整体风格定位
|
|
|
+2. 叙事视角选择
|
|
|
+3. 语言特点(句式、用词、描写侧重)
|
|
|
+4. 节奏控制
|
|
|
+5. 禁忌与避免
|
|
|
+
|
|
|
+格式为 Markdown,内容要具体、详细、有创意。`;
|
|
|
+
|
|
|
+ const response = await fetch(apiSettings.apiUrl, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ Authorization: `Bearer ${apiSettings.apiKey}`,
|
|
|
+ },
|
|
|
+ body: JSON.stringify({
|
|
|
+ model: apiSettings.model,
|
|
|
+ messages: [{ role: 'user', content: prompt }],
|
|
|
+ max_tokens: 2000,
|
|
|
+ }),
|
|
|
+ signal: AbortSignal.timeout(60000),
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`AI request failed with status ${response.status}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const data = await response.json();
|
|
|
+ const content = data.choices?.[0]?.message?.content;
|
|
|
+
|
|
|
+ if (!content || typeof content !== 'string' || content.trim() === '') {
|
|
|
+ throw new Error('AI returned empty content');
|
|
|
+ }
|
|
|
+
|
|
|
+ return content.trim();
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 原子写入文件(先写临时文件,再重命名)
|
|
|
+ */
|
|
|
+async function atomicWrite(filePath: string, content: string): Promise<void> {
|
|
|
+ const tmpPath = `${filePath}.tmp`;
|
|
|
+ await fs.writeFile(tmpPath, content, 'utf-8');
|
|
|
+ await fs.rename(tmpPath, filePath);
|
|
|
+}
|
|
|
+
|
|
|
+export async function POST(request: NextRequest) {
|
|
|
+ let body: InitRequest;
|
|
|
+
|
|
|
+ try {
|
|
|
+ body = await request.json();
|
|
|
+ } catch {
|
|
|
+ return NextResponse.json({ success: false, error: '无效的请求体' }, { status: 400 });
|
|
|
+ }
|
|
|
+
|
|
|
+ const { projectPath, novelType, worldDescription, apiSettings, disabledFiles = [] } = body;
|
|
|
+
|
|
|
+ // 1. 路径安全验证
|
|
|
+ if (!projectPath || !isPathSafe(projectPath)) {
|
|
|
+ return NextResponse.json({ success: false, error: '无效的项目路径' }, { status: 400 });
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 2. 创建目录结构
|
|
|
+ const dirs = [
|
|
|
+ path.join(projectPath, 'world_state'),
|
|
|
+ path.join(projectPath, 'world_state', 'lore'),
|
|
|
+ path.join(projectPath, 'chapters'),
|
|
|
+ path.join(projectPath, 'control-cards'),
|
|
|
+ path.join(projectPath, 'logs'),
|
|
|
+ path.join(projectPath, 'history'),
|
|
|
+ path.join(projectPath, 'event-graph'),
|
|
|
+ path.join(projectPath, 'world_state', 'rolling-summaries'),
|
|
|
+ path.join(projectPath, 'logs', 'audit'),
|
|
|
+ path.join(projectPath, 'logs', 'reader-feedback'),
|
|
|
+ ];
|
|
|
+
|
|
|
+ for (const dir of dirs) {
|
|
|
+ await fs.mkdir(dir, { recursive: true });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 需求 1.10:获取按 Novel_Type 差异化的模板内容
|
|
|
+ const novelTypeTemplates = getNovelTypeTemplate(novelType);
|
|
|
+
|
|
|
+ // 3. 准备所有要写入的文件(一次性收集)
|
|
|
+ const filesToWrite: Array<{ filePath: string; content: string }> = [];
|
|
|
+
|
|
|
+ // 3.1 World_State_File 模板(跳过用户禁用的文件)
|
|
|
+ for (const fileName of DEFAULT_WORLD_STATE_FILES) {
|
|
|
+ if (disabledFiles.includes(fileName)) continue;
|
|
|
+ const filePath = path.join(projectPath, 'world_state', fileName);
|
|
|
+ const template = novelTypeTemplates[fileName] ?? getDefaultTemplate(fileName);
|
|
|
+ filesToWrite.push({ filePath, content: template });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3.2 World_Lore_File 模板(跳过用户禁用的文件)
|
|
|
+ for (const fileName of DEFAULT_LORE_FILES) {
|
|
|
+ if (disabledFiles.includes(fileName)) continue;
|
|
|
+ const filePath = path.join(projectPath, 'world_state', 'lore', fileName);
|
|
|
+ const template = getDefaultTemplate(fileName);
|
|
|
+ filesToWrite.push({ filePath, content: template });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3.3 AI 生成内容(如果提供了 apiSettings 和 worldDescription)
|
|
|
+ const generatedFiles: string[] = [];
|
|
|
+ const failedFiles: string[] = [];
|
|
|
+
|
|
|
+ if (apiSettings && worldDescription) {
|
|
|
+ // 只生成文风设定(writing_style.md),世界观和世界状态使用默认模板
|
|
|
+ const generateWritingStyle = !disabledFiles.includes('writing_style.md');
|
|
|
+
|
|
|
+ if (generateWritingStyle) {
|
|
|
+ const filePath = path.join(projectPath, 'world_state', 'writing_style.md');
|
|
|
+
|
|
|
+ try {
|
|
|
+ const content = await generateWithAI(
|
|
|
+ 'writing_style.md',
|
|
|
+ worldDescription,
|
|
|
+ novelType,
|
|
|
+ apiSettings,
|
|
|
+ body.writingStylePreset,
|
|
|
+ body.writingStyleDescription
|
|
|
+ );
|
|
|
+ // 更新 filesToWrite 中的对应文件内容
|
|
|
+ const existingIndex = filesToWrite.findIndex((f) => f.filePath === filePath);
|
|
|
+ if (existingIndex >= 0) {
|
|
|
+ filesToWrite[existingIndex].content = content;
|
|
|
+ }
|
|
|
+ generatedFiles.push('writing_style.md');
|
|
|
+ } catch {
|
|
|
+ // 失败则保留模板内容(错误隔离)
|
|
|
+ failedFiles.push('writing_style.md');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3.3b Generate iron-rules.md via the dedicated API
|
|
|
+ if (body.generateIronRules && apiSettings && worldDescription) {
|
|
|
+ const ironRulesPath = path.join(projectPath, 'iron-rules.md');
|
|
|
+ try {
|
|
|
+ const origin = request.headers.get('origin') || `http://localhost:${process.env.PORT || 3000}`;
|
|
|
+ const ironRulesRes = await fetch(`${origin}/api/novel/iron-rules`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({
|
|
|
+ projectInfo: {
|
|
|
+ novelType,
|
|
|
+ worldDescription: worldDescription || '',
|
|
|
+ protagonistDescription: body.interviewData?.characters || '',
|
|
|
+ plotDirection: body.interviewData?.plotDirection || '',
|
|
|
+ },
|
|
|
+ apiSettings,
|
|
|
+ }),
|
|
|
+ });
|
|
|
+ const ironRulesData = await ironRulesRes.json();
|
|
|
+ if (ironRulesData.success) {
|
|
|
+ // Replace in filesToWrite or add new
|
|
|
+ const existingIdx = filesToWrite.findIndex(f => f.filePath === ironRulesPath);
|
|
|
+ if (existingIdx >= 0) {
|
|
|
+ filesToWrite[existingIdx].content = ironRulesData.markdown;
|
|
|
+ } else {
|
|
|
+ filesToWrite.push({ filePath: ironRulesPath, content: ironRulesData.markdown });
|
|
|
+ }
|
|
|
+ generatedFiles.push('iron-rules.md');
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ failedFiles.push('iron-rules.md');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3.3c Generate style-anchor.md via the dedicated API
|
|
|
+ if (body.generateStyleAnchor && apiSettings && body.styleInfo) {
|
|
|
+ const styleAnchorPath = path.join(projectPath, 'style-anchor.md');
|
|
|
+ try {
|
|
|
+ const origin = request.headers.get('origin') || `http://localhost:${process.env.PORT || 3000}`;
|
|
|
+ const styleAnchorRes = await fetch(`${origin}/api/novel/style-anchor`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({
|
|
|
+ styleInfo: {
|
|
|
+ ...body.styleInfo,
|
|
|
+ novelType,
|
|
|
+ worldDescription: worldDescription || '',
|
|
|
+ },
|
|
|
+ apiSettings,
|
|
|
+ }),
|
|
|
+ });
|
|
|
+ const styleAnchorData = await styleAnchorRes.json();
|
|
|
+ if (styleAnchorData.success) {
|
|
|
+ const existingIdx = filesToWrite.findIndex(f => f.filePath === styleAnchorPath);
|
|
|
+ if (existingIdx >= 0) {
|
|
|
+ filesToWrite[existingIdx].content = styleAnchorData.markdown;
|
|
|
+ } else {
|
|
|
+ filesToWrite.push({ filePath: styleAnchorPath, content: styleAnchorData.markdown });
|
|
|
+ }
|
|
|
+ generatedFiles.push('style-anchor.md');
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ failedFiles.push('style-anchor.md');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3.3d Write workflow-state.json
|
|
|
+ filesToWrite.push({
|
|
|
+ filePath: path.join(projectPath, 'workflow-state.json'),
|
|
|
+ content: JSON.stringify({
|
|
|
+ phase: 'phase1',
|
|
|
+ totalChapters: 0,
|
|
|
+ lastChapterIndex: 0,
|
|
|
+ lastSummarizedChapter: 0,
|
|
|
+ lastAuditedChapter: 0,
|
|
|
+ pendingEventIds: [],
|
|
|
+ milestoneAuditHistory: [],
|
|
|
+ }, null, 2),
|
|
|
+ });
|
|
|
+
|
|
|
+ // 3.3e Create additional directories
|
|
|
+ const extraDirs = [
|
|
|
+ path.join(projectPath, 'event-graph'),
|
|
|
+ path.join(projectPath, 'world_state', 'rolling-summaries'),
|
|
|
+ path.join(projectPath, 'logs', 'audit'),
|
|
|
+ path.join(projectPath, 'logs', 'reader-feedback'),
|
|
|
+ ];
|
|
|
+ for (const dir of extraDirs) {
|
|
|
+ await fs.mkdir(dir, { recursive: true });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3.3f Write empty event graph files
|
|
|
+ filesToWrite.push({
|
|
|
+ filePath: path.join(projectPath, 'event-graph', 'events.json'),
|
|
|
+ content: '[]',
|
|
|
+ });
|
|
|
+ filesToWrite.push({
|
|
|
+ filePath: path.join(projectPath, 'event-graph', 'edges.json'),
|
|
|
+ content: '[]',
|
|
|
+ });
|
|
|
+ filesToWrite.push({
|
|
|
+ filePath: path.join(projectPath, 'event-graph', 'chapter-allocation.json'),
|
|
|
+ content: '[]',
|
|
|
+ });
|
|
|
+
|
|
|
+ // 3.4 novel_project.json(如果不存在)
|
|
|
+ const projectConfigPath = path.join(projectPath, 'novel_project.json');
|
|
|
+ let configExists = false;
|
|
|
+ try {
|
|
|
+ await fs.access(projectConfigPath);
|
|
|
+ configExists = true;
|
|
|
+ } catch {
|
|
|
+ configExists = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!configExists) {
|
|
|
+ const disabledWorldStateFiles = disabledFiles.filter((f) =>
|
|
|
+ (DEFAULT_WORLD_STATE_FILES as readonly string[]).includes(f)
|
|
|
+ );
|
|
|
+
|
|
|
+ const projectConfig = {
|
|
|
+ name: path.basename(projectPath),
|
|
|
+ novelType,
|
|
|
+ createdAt: new Date().toISOString(),
|
|
|
+ lastOpenedAt: new Date().toISOString(),
|
|
|
+ chapterWordCountMin: 2000,
|
|
|
+ chapterWordCountMax: 4000,
|
|
|
+ recentChaptersCount: 3,
|
|
|
+ disabledWorldStateFiles,
|
|
|
+ writingConstraints: [],
|
|
|
+ antiHomogenizationStrategies: [],
|
|
|
+ lastUsedPromptTemplate: '',
|
|
|
+ };
|
|
|
+ filesToWrite.push({ filePath: projectConfigPath, content: JSON.stringify(projectConfig, null, 2) });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 一次性写入所有文件
|
|
|
+ for (const { filePath, content } of filesToWrite) {
|
|
|
+ await atomicWrite(filePath, content);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 返回结果
|
|
|
+ return NextResponse.json({
|
|
|
+ success: true,
|
|
|
+ generatedFiles,
|
|
|
+ failedFiles,
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ const message = error instanceof Error ? error.message : '初始化失败';
|
|
|
+ return NextResponse.json({ success: false, error: message }, { status: 500 });
|
|
|
+ }
|
|
|
+}
|