route.ts 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import { NextRequest, NextResponse } from 'next/server';
  2. import { isPathSafe } from '@/app/lib/novel-server-utils';
  3. import { promises as fs } from 'fs';
  4. import path from 'path';
  5. export async function POST(request: NextRequest) {
  6. const body = await request.json().catch(() => null);
  7. if (!body) return NextResponse.json({ success: false, error: '无效请求' }, { status: 400 });
  8. const { projectPath, chapterIndex, chapterContent, apiSettings } = body;
  9. if (!projectPath || !isPathSafe(projectPath) || !apiSettings || !chapterContent) {
  10. return NextResponse.json({ success: false, error: '缺少必要参数' }, { status: 400 });
  11. }
  12. // Only trigger every 5 chapters, but allow override
  13. if (chapterIndex % 5 !== 0 && !body.force) {
  14. return NextResponse.json({ success: true, skipped: true });
  15. }
  16. // Get recent chapters for context
  17. let recentContext = '';
  18. const startChapter = Math.max(1, chapterIndex - 4);
  19. const chaptersDir = path.join(projectPath, 'chapters');
  20. try {
  21. const files = await fs.readdir(chaptersDir);
  22. for (const file of files) {
  23. const match = file.match(/^chapter_(\d+)_/);
  24. if (match) {
  25. const idx = parseInt(match[1]);
  26. if (idx >= startChapter && idx <= chapterIndex) {
  27. const content = await fs.readFile(path.join(chaptersDir, file), 'utf-8');
  28. recentContext += `\n### 第${idx}章\n${content.slice(0, 800)}\n`;
  29. }
  30. }
  31. }
  32. } catch { /* continue without context */ }
  33. const prompt = `你是一位付费阅读的资深读者。请对以下小说章节给出读者反馈。
  34. ## 最近章节(摘要)
  35. ${recentContext.slice(0, 3000)}
  36. ## 当前章节
  37. ${chapterContent.slice(0, 3000)}
  38. ## 评分维度(1-10分)
  39. 1. 节奏感:情节推进是否紧凑?是否拖沓或过赶?
  40. 2. 爽点密度:情绪释放是否到位?是否有满足感的峰值?
  41. 3. 情感共鸣:是否关心角色的命运?是否有打动人的时刻?
  42. 4. 阅读困惑:哪些地方可能需要回看才能理解?是否有逻辑断层?
  43. ## 输出格式
  44. 只返回 JSON:
  45. {
  46. "pace": 7,
  47. "excitementDensity": 6,
  48. "emotionalEngagement": 8,
  49. "confusion": 3,
  50. "suggestions": "具体的读者感受和建议,2-3句话"
  51. }`;
  52. try {
  53. const response = await fetch(apiSettings.apiUrl, {
  54. method: 'POST',
  55. headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiSettings.apiKey}` },
  56. body: JSON.stringify({ model: apiSettings.model, messages: [{ role: 'user', content: prompt }], max_tokens: 500 }),
  57. signal: AbortSignal.timeout(55000),
  58. });
  59. if (!response.ok) {
  60. return NextResponse.json({ success: false, error: `AI 请求失败: ${response.status}` }, { status: 502 });
  61. }
  62. const data = await response.json();
  63. const text = data.choices?.[0]?.message?.content || '';
  64. const jsonMatch = text.match(/\{[\s\S]*\}/);
  65. let feedback;
  66. try {
  67. feedback = jsonMatch ? JSON.parse(jsonMatch[0]) : null;
  68. } catch {
  69. return NextResponse.json({ success: false, error: 'AI 返回了无效的 JSON 响应' }, { status: 502 });
  70. }
  71. // Save feedback to file
  72. const feedbackDir = path.join(projectPath, 'logs', 'reader-feedback');
  73. await fs.mkdir(feedbackDir, { recursive: true });
  74. const range = `${startChapter}-${chapterIndex}`;
  75. await fs.writeFile(
  76. path.join(feedbackDir, `chapters-${range}.json`),
  77. JSON.stringify({ ...feedback, chapterRange: range }, null, 2),
  78. 'utf-8'
  79. );
  80. return NextResponse.json({ success: true, feedback });
  81. } catch (error) {
  82. return NextResponse.json({ success: false, error: (error as Error).message }, { status: 500 });
  83. }
  84. }