Explorar el Código

feat: add iron-rules + style-anchor + workflow-state generation to Phase 0 init

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nonvioce hace 6 meses
padre
commit
34420d1f22
Se han modificado 2 ficheros con 415 adiciones y 230 borrados
  1. 353 229
      src/app/api/novel/init/route.ts
  2. 62 1
      src/app/features/novel-writer/components/InitWizard.tsx

+ 353 - 229
src/app/api/novel/init/route.ts

@@ -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 });
+  }
+}

+ 62 - 1
src/app/features/novel-writer/components/InitWizard.tsx

@@ -279,6 +279,9 @@ function Step5StyleAndGenerate({
   primaryStyleId, setPrimaryStyleId, supportStyleId, setSupportStyleId,
   primaryStyleId, setPrimaryStyleId, supportStyleId, setSupportStyleId,
   styleIntensity, setStyleIntensity, paragraphMode, setParagraphMode,
   styleIntensity, setStyleIntensity, paragraphMode, setParagraphMode,
   useChapterTitles, setUseChapterTitles,
   useChapterTitles, setUseChapterTitles,
+  generateIronRules, setGenerateIronRules,
+  generateStyleAnchor, setGenerateStyleAnchor,
+  includeNegativeSamples, setIncludeNegativeSamples,
   onBack, onComplete,
   onBack, onComplete,
 }: {
 }: {
   projectPath: string; novelType: NovelType; worldDescription: string;
   projectPath: string; novelType: NovelType; worldDescription: string;
@@ -290,6 +293,9 @@ function Step5StyleAndGenerate({
   styleIntensity: number; setStyleIntensity: (v: number) => void;
   styleIntensity: number; setStyleIntensity: (v: number) => void;
   paragraphMode: 'web-serial-natural' | 'long-paragraph'; setParagraphMode: (v: 'web-serial-natural' | 'long-paragraph') => void;
   paragraphMode: 'web-serial-natural' | 'long-paragraph'; setParagraphMode: (v: 'web-serial-natural' | 'long-paragraph') => void;
   useChapterTitles: boolean; setUseChapterTitles: (v: boolean) => void;
   useChapterTitles: boolean; setUseChapterTitles: (v: boolean) => void;
+  generateIronRules: boolean; setGenerateIronRules: (v: boolean) => void;
+  generateStyleAnchor: boolean; setGenerateStyleAnchor: (v: boolean) => void;
+  includeNegativeSamples: boolean; setIncludeNegativeSamples: (v: boolean) => void;
   onBack: () => void; onComplete: () => void;
   onBack: () => void; onComplete: () => void;
 }) {
 }) {
   const [fileStates] = useState<FileGenerationState[]>(
   const [fileStates] = useState<FileGenerationState[]>(
@@ -328,6 +334,21 @@ function Step5StyleAndGenerate({
           writingStylePreset: selectedPreset || undefined,
           writingStylePreset: selectedPreset || undefined,
           writingStyleDescription: enrichedDescription || undefined,
           writingStyleDescription: enrichedDescription || undefined,
           disabledFiles,
           disabledFiles,
+          generateIronRules,
+          generateStyleAnchor,
+          styleInfo: {
+            primaryStyle: primaryStyleId,
+            supportStyle: supportStyleId,
+            intensity: styleIntensity,
+            styleDescription: styleDescription || '',
+            paragraphMode: paragraphMode,
+            chapterTitles: useChapterTitles,
+            novelTypeName: novelType,
+          },
+          interviewData: {
+            characters: `${interviewData.protagonist}\n\n核心性格:${interviewData.personality}\n\n核心阵容:${interviewData.coreCast}`,
+            plotDirection: `${interviewData.primaryConflict}\n\n结局方向:${interviewData.endingDirection}`,
+          },
         }),
         }),
       });
       });
 
 
@@ -344,7 +365,7 @@ function Step5StyleAndGenerate({
     } finally {
     } finally {
       setIsRunning(false);
       setIsRunning(false);
     }
     }
-  }, [projectPath, novelType, worldDescription, apiSettings, selectedPreset, styleDescription, disabledFiles, interviewData, primaryStyleId, supportStyleId, styleIntensity, paragraphMode, useChapterTitles]);
+  }, [projectPath, novelType, worldDescription, apiSettings, selectedPreset, styleDescription, disabledFiles, interviewData, primaryStyleId, supportStyleId, styleIntensity, paragraphMode, useChapterTitles, generateIronRules, generateStyleAnchor]);
 
 
   return (
   return (
     <div>
     <div>
@@ -427,6 +448,40 @@ function Step5StyleAndGenerate({
         </div>
         </div>
       </div>
       </div>
 
 
+      {/* 项目奠基选项 */}
+      <div className="mt-6 p-4 bg-indigo-50 border border-indigo-200 rounded-xl space-y-3">
+        <h4 className="text-sm font-semibold text-indigo-800">项目奠基</h4>
+        <label className="flex items-center gap-2 text-sm text-gray-700 cursor-pointer">
+          <input
+            type="checkbox"
+            checked={generateIronRules}
+            onChange={(e) => setGenerateIronRules(e.target.checked)}
+            className="w-4 h-4 text-indigo-600 rounded"
+          />
+          自动生成项目铁律 (iron-rules.md)
+        </label>
+        <label className="flex items-center gap-2 text-sm text-gray-700 cursor-pointer">
+          <input
+            type="checkbox"
+            checked={generateStyleAnchor}
+            onChange={(e) => setGenerateStyleAnchor(e.target.checked)}
+            className="w-4 h-4 text-indigo-600 rounded"
+          />
+          自动生成风格锚定 (style-anchor.md)
+        </label>
+        {generateStyleAnchor && (
+          <label className="flex items-center gap-2 text-sm text-gray-600 ml-6 cursor-pointer">
+            <input
+              type="checkbox"
+              checked={includeNegativeSamples}
+              onChange={(e) => setIncludeNegativeSamples(e.target.checked)}
+              className="w-3.5 h-3.5 text-indigo-500 rounded"
+            />
+            包含负面示范样本
+          </label>
+        )}
+      </div>
+
       {globalError && <p className="text-sm text-red-500 mb-4">{globalError}</p>}
       {globalError && <p className="text-sm text-red-500 mb-4">{globalError}</p>}
 
 
       {!isRunning && !allDone && (
       {!isRunning && !allDone && (
@@ -572,6 +627,9 @@ export default function InitWizard({ projectPath, novelType: initialNovelType, a
   const [styleIntensity, setStyleIntensity] = useState(5);
   const [styleIntensity, setStyleIntensity] = useState(5);
   const [paragraphMode, setParagraphMode] = useState<'web-serial-natural' | 'long-paragraph'>('web-serial-natural');
   const [paragraphMode, setParagraphMode] = useState<'web-serial-natural' | 'long-paragraph'>('web-serial-natural');
   const [useChapterTitles, setUseChapterTitles] = useState(true);
   const [useChapterTitles, setUseChapterTitles] = useState(true);
+  const [generateIronRules, setGenerateIronRules] = useState(true);
+  const [generateStyleAnchor, setGenerateStyleAnchor] = useState(true);
+  const [includeNegativeSamples, setIncludeNegativeSamples] = useState(true);
 
 
   const interviewData: InterviewData = {
   const interviewData: InterviewData = {
     protagonist, personality, coreCast,
     protagonist, personality, coreCast,
@@ -627,6 +685,9 @@ export default function InitWizard({ projectPath, novelType: initialNovelType, a
             styleIntensity={styleIntensity} setStyleIntensity={setStyleIntensity}
             styleIntensity={styleIntensity} setStyleIntensity={setStyleIntensity}
             paragraphMode={paragraphMode} setParagraphMode={setParagraphMode}
             paragraphMode={paragraphMode} setParagraphMode={setParagraphMode}
             useChapterTitles={useChapterTitles} setUseChapterTitles={setUseChapterTitles}
             useChapterTitles={useChapterTitles} setUseChapterTitles={setUseChapterTitles}
+            generateIronRules={generateIronRules} setGenerateIronRules={setGenerateIronRules}
+            generateStyleAnchor={generateStyleAnchor} setGenerateStyleAnchor={setGenerateStyleAnchor}
+            includeNegativeSamples={includeNegativeSamples} setIncludeNegativeSamples={setIncludeNegativeSamples}
             onBack={() => setStep(4)} onComplete={() => onComplete(interviewData)} />
             onBack={() => setStep(4)} onComplete={() => onComplete(interviewData)} />
         )}
         )}
       </div>
       </div>