| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798 |
- 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, chapterContent, apiSettings } = body;
- if (!projectPath || !isPathSafe(projectPath) || !apiSettings || !chapterContent) {
- return NextResponse.json({ success: false, error: '缺少必要参数' }, { status: 400 });
- }
- // Only trigger every 5 chapters, but allow override
- if (chapterIndex % 5 !== 0 && !body.force) {
- return NextResponse.json({ success: true, skipped: true });
- }
- // Get recent chapters for context
- let recentContext = '';
- const startChapter = Math.max(1, chapterIndex - 4);
- const chaptersDir = path.join(projectPath, 'chapters');
- try {
- const files = await fs.readdir(chaptersDir);
- for (const file of files) {
- const match = file.match(/^chapter_(\d+)_/);
- if (match) {
- const idx = parseInt(match[1]);
- if (idx >= startChapter && idx <= chapterIndex) {
- const content = await fs.readFile(path.join(chaptersDir, file), 'utf-8');
- recentContext += `\n### 第${idx}章\n${content.slice(0, 800)}\n`;
- }
- }
- }
- } catch { /* continue without context */ }
- const prompt = `你是一位付费阅读的资深读者。请对以下小说章节给出读者反馈。
- ## 最近章节(摘要)
- ${recentContext.slice(0, 3000)}
- ## 当前章节
- ${chapterContent.slice(0, 3000)}
- ## 评分维度(1-10分)
- 1. 节奏感:情节推进是否紧凑?是否拖沓或过赶?
- 2. 爽点密度:情绪释放是否到位?是否有满足感的峰值?
- 3. 情感共鸣:是否关心角色的命运?是否有打动人的时刻?
- 4. 阅读困惑:哪些地方可能需要回看才能理解?是否有逻辑断层?
- ## 输出格式
- 只返回 JSON:
- {
- "pace": 7,
- "excitementDensity": 6,
- "emotionalEngagement": 8,
- "confusion": 3,
- "suggestions": "具体的读者感受和建议,2-3句话"
- }`;
- 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: 500 }),
- 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]*\}/);
- let feedback;
- try {
- feedback = jsonMatch ? JSON.parse(jsonMatch[0]) : null;
- } catch {
- return NextResponse.json({ success: false, error: 'AI 返回了无效的 JSON 响应' }, { status: 502 });
- }
- // Save feedback to file
- const feedbackDir = path.join(projectPath, 'logs', 'reader-feedback');
- await fs.mkdir(feedbackDir, { recursive: true });
- const range = `${startChapter}-${chapterIndex}`;
- await fs.writeFile(
- path.join(feedbackDir, `chapters-${range}.json`),
- JSON.stringify({ ...feedback, chapterRange: range }, null, 2),
- 'utf-8'
- );
- return NextResponse.json({ success: true, feedback });
- } catch (error) {
- return NextResponse.json({ success: false, error: (error as Error).message }, { status: 500 });
- }
- }
|