|
@@ -0,0 +1,82 @@
|
|
|
|
|
+import { NextRequest, NextResponse } from 'next/server';
|
|
|
|
|
+import { isPathSafe } from '@/app/lib/novel-server-utils';
|
|
|
|
|
+import { promises as fs } from 'fs';
|
|
|
|
|
+import path from 'path';
|
|
|
|
|
+
|
|
|
|
|
+export async function POST(request: NextRequest) {
|
|
|
|
|
+ const body = await request.json().catch(() => null);
|
|
|
|
|
+ if (!body) return NextResponse.json({ success: false, error: '无效请求' }, { status: 400 });
|
|
|
|
|
+
|
|
|
|
|
+ const { projectPath, chapterIndex, apiSettings } = body;
|
|
|
|
|
+ if (!projectPath || !isPathSafe(projectPath) || !apiSettings) {
|
|
|
|
|
+ return NextResponse.json({ success: false, error: '缺少必要参数' }, { status: 400 });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let ironRules = '', styleAnchor = '';
|
|
|
|
|
+ try {
|
|
|
|
|
+ ironRules = await fs.readFile(path.join(projectPath, 'iron-rules.md'), 'utf-8').catch(() => '');
|
|
|
|
|
+ styleAnchor = await fs.readFile(path.join(projectPath, 'style-anchor.md'), 'utf-8').catch(() => '');
|
|
|
|
|
+ } catch { /* ignore */ }
|
|
|
|
|
+
|
|
|
|
|
+ let summaries = '';
|
|
|
|
|
+ try {
|
|
|
|
|
+ const summaryDir = path.join(projectPath, 'world_state', 'rolling-summaries');
|
|
|
|
|
+ const files = (await fs.readdir(summaryDir).catch(() => [] as string[])).sort();
|
|
|
|
|
+ for (const file of files) {
|
|
|
|
|
+ summaries += await fs.readFile(path.join(summaryDir, file), 'utf-8') + '\n\n';
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch { /* ignore */ }
|
|
|
|
|
+
|
|
|
|
|
+ const prompt = `对第1-${chapterIndex}章进行一次全书锚定审计。
|
|
|
|
|
+
|
|
|
|
|
+## 项目铁律
|
|
|
|
|
+${ironRules || '(未设置)'}
|
|
|
|
|
+
|
|
|
|
|
+## 风格锚定(摘要)
|
|
|
|
|
+${styleAnchor.slice(0, 2000) || '(未设置)'}
|
|
|
|
|
+
|
|
|
|
|
+## 滚动摘要
|
|
|
|
|
+${summaries.slice(0, 5000)}
|
|
|
|
|
+
|
|
|
|
|
+## 审计维度
|
|
|
|
|
+1. 角色漂移:主角性格/语言/行为是否在过程中逐渐偏离初始设定?
|
|
|
|
|
+2. 设定漂移:世界规则是否在后期出现不一致或隐性修改?
|
|
|
|
|
+3. 风格漂移:文风是否逐渐趋向 AI 模式化?
|
|
|
|
|
+4. 伏笔缺口:已埋未收的伏笔是否过多?
|
|
|
|
|
+
|
|
|
|
|
+## 输出格式
|
|
|
|
|
+只返回 JSON:
|
|
|
|
|
+{"characterDrift":{"severity":"ok|warning|issue","detail":"具体情况"},"settingDrift":{"severity":"ok|warning|issue","detail":"具体情况"},"styleDrift":{"severity":"ok|warning|issue","detail":"具体情况"},"foreshadowingGap":{"severity":"ok|warning|issue","detail":"具体情况"},"overallAssessment":"总体评估和建议"}`;
|
|
|
|
|
+
|
|
|
|
|
+ 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: 2000 }),
|
|
|
|
|
+ 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 || '';
|
|
|
|
|
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
|
|
|
+ const audit = jsonMatch ? JSON.parse(jsonMatch[0]) : null;
|
|
|
|
|
+
|
|
|
|
|
+ if (audit) {
|
|
|
|
|
+ const auditDir = path.join(projectPath, 'logs', 'audit');
|
|
|
|
|
+ await fs.mkdir(auditDir, { recursive: true });
|
|
|
|
|
+ await fs.writeFile(
|
|
|
|
|
+ path.join(auditDir, `milestone-${chapterIndex}.json`),
|
|
|
|
|
+ JSON.stringify({ ...audit, chapterRange: `1-${chapterIndex}` }, null, 2),
|
|
|
|
|
+ 'utf-8'
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return NextResponse.json({ success: true, audit });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ return NextResponse.json({ success: false, error: (error as Error).message }, { status: 500 });
|
|
|
|
|
+ }
|
|
|
|
|
+}
|