Răsfoiți Sursa

feat: add iron-rules generation API with prompt builder

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nonvioce 6 luni în urmă
părinte
comite
3c3e69067a

+ 63 - 0
src/app/api/novel/iron-rules/route.ts

@@ -0,0 +1,63 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { buildIronRulesPrompt } from '@/app/lib/chapter-prompts';
+
+export async function POST(request: NextRequest) {
+  const body = await request.json().catch(() => null);
+  if (!body) return NextResponse.json({ success: false, error: '无效的请求体' }, { status: 400 });
+
+  const { projectInfo, apiSettings } = body;
+  if (!projectInfo || !apiSettings) {
+    return NextResponse.json({ success: false, error: '缺少参数' }, { status: 400 });
+  }
+
+  const prompt = buildIronRulesPrompt(projectInfo);
+
+  try {
+    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: 1000,
+      }),
+      signal: AbortSignal.timeout(55000),
+    });
+
+    if (!response.ok) {
+      return NextResponse.json({ success: false, error: `AI 请求失败: ${response.status}` }, { status: 502 });
+    }
+
+    const data = await response.json();
+    const text = data.choices?.[0]?.message?.content || '';
+
+    // Parse JSON from response (handle markdown code fences)
+    const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/) || [null, text];
+    const rules = JSON.parse(jsonMatch[1].trim());
+
+    // Format as markdown
+    const categories: Record<string, string> = {
+      character: '角色铁律',
+      worldbuilding: '世界观铁律',
+      plot: '情节铁律',
+      style: '风格铁律',
+    };
+    let markdown = '# 项目铁律\n\n';
+    for (const [cat, label] of Object.entries(categories)) {
+      const catRules = rules.filter((r: { category: string }) => r.category === cat);
+      if (catRules.length === 0) continue;
+      markdown += `## ${label}\n\n`;
+      for (const r of catRules) {
+        markdown += `- ${r.rule}\n`;
+      }
+      markdown += '\n';
+    }
+
+    return NextResponse.json({ success: true, rules, markdown });
+  } catch (error) {
+    return NextResponse.json({ success: false, error: (error as Error).message }, { status: 500 });
+  }
+}

+ 27 - 0
src/app/lib/__tests__/iron-rules.test.ts

@@ -0,0 +1,27 @@
+import { describe, it, expect } from 'vitest';
+import { buildIronRulesPrompt } from '@/app/lib/chapter-prompts';
+
+describe('buildIronRulesPrompt', () => {
+  it('includes all project info in the prompt', () => {
+    const prompt = buildIronRulesPrompt({
+      novelType: 'xianxia',
+      worldDescription: '修仙世界',
+      protagonistDescription: '少年剑客',
+      plotDirection: '复仇与成长',
+    });
+    expect(prompt).toContain('xianxia');
+    expect(prompt).toContain('修仙世界');
+    expect(prompt).toContain('少年剑客');
+    expect(prompt).toContain('复仇与成长');
+  });
+
+  it('handles empty fields gracefully', () => {
+    const prompt = buildIronRulesPrompt({
+      novelType: 'custom',
+      worldDescription: '',
+      protagonistDescription: '',
+      plotDirection: '',
+    });
+    expect(prompt).toContain('未提供');
+  });
+});

+ 37 - 0
src/app/lib/chapter-prompts.ts

@@ -263,3 +263,40 @@ export const INTERVIEW_QUESTIONS = {
     ],
   },
 };
+
+// ─── Iron Rules Generation ───────────────────────────────────────
+
+export function buildIronRulesPrompt(projectInfo: {
+  novelType: string;
+  worldDescription: string;
+  protagonistDescription: string;
+  plotDirection: string;
+}): string {
+  return `你是一位资深小说编辑。请根据以下项目设定,为这部长篇小说生成5-8条项目铁律。
+
+## 项目信息
+- 类型:${projectInfo.novelType}
+- 世界观:${projectInfo.worldDescription || '未提供'}
+- 主角设定:${projectInfo.protagonistDescription || '未提供'}
+- 情节方向:${projectInfo.plotDirection || '未提供'}
+
+## 铁律分类
+1. 角色铁律(character):人物行为边界、性格一致性规则
+2. 世界观铁律(worldbuilding):世界规则不可违背的底线
+3. 情节铁律(plot):情节发展不能触碰的逻辑禁区
+4. 风格铁律(style):文风和语言必须遵守的规则
+
+## 要求
+- 每条铁律必须具体、可检查、有约束力
+- 避免"保持人物性格一致"这种空泛表述
+- 用否定句或条件句增加约束力,如"禁止主角在未铺垫的情况下突然获得关键能力"
+- 每条标注分类
+
+## 输出格式
+返回 JSON 数组(只返回 JSON,不要其他文字):
+[
+  { "id": "rule-1", "rule": "...", "category": "character" },
+  { "id": "rule-2", "rule": "...", "category": "worldbuilding" },
+  ...
+]`;
+}