@waterwx/dsh-novel-forge 1.1.3 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/routes.ts CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  type AssistantHistoryResponse,
23
23
  type AssistantRequest,
24
24
  type AuditResponse,
25
+ type AuthorReview,
25
26
  type BiblePatchRequest,
26
27
  type BibleRequest,
27
28
  type BlurbRequest,
@@ -74,6 +75,8 @@ import { BUILTIN_ANTI_AI_RULES, BUILTIN_GENRE_LIBRARY, BUILTIN_PROGRESSION_MODES
74
75
  import {
75
76
  chapterFileName,
76
77
  auditBook,
78
+ authorReviewChapter,
79
+ autoLinkPlotlines,
77
80
  backfillFacts,
78
81
  createProject,
79
82
  exportBook,
@@ -89,12 +92,16 @@ import {
89
92
  polishChapterStream,
90
93
  readChapterFile,
91
94
  refreshCharacters,
95
+ refreshPlotlineProgress,
92
96
  reviewChapter,
93
97
  reviewChapterText,
94
98
  rewriteChapterStream,
95
99
  saveProject,
100
+ analyzePlotlineHealth,
101
+ designPlotlinePlan,
96
102
  checkSensitiveText,
97
103
  suggestForeshadows,
104
+ suggestPlotlines,
98
105
  summarizeAndExtractFacts,
99
106
  summarizeChapter,
100
107
  syncProjectWithDisk,
@@ -445,6 +452,31 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
445
452
  chapter.status = 'approved'
446
453
  saveProject(config.outputDir, project)
447
454
  }
455
+ // 作者复盘:叙事结构检查(钩子兑现/结尾钩子/推进/连续性/趋势;不改变章节状态)。
456
+ if (config.autoAuthorReview ?? true) {
457
+ try {
458
+ const currentBody = readChapterFile(config.outputDir, chapter)
459
+ let prevTail = ''
460
+ if (no > 1) {
461
+ const prev = project.chapters.find(c => c.no === no - 1)
462
+ if (prev !== undefined) {
463
+ prevTail = (readChapterFile(config.outputDir, prev) ?? '').replace(/^#.*$/m, '').trim().slice(-600)
464
+ }
465
+ }
466
+ if (currentBody !== undefined) {
467
+ const review = await authorReviewChapter(ctx, config, project, no, currentBody, prevTail)
468
+ chapter.authorReview = review
469
+ // 复盘标记推进的剧情线 → 自动关联本章。
470
+ if (review.advancedLines !== undefined) {
471
+ autoLinkPlotlines(project, no, review.advancedLines)
472
+ }
473
+ saveProject(config.outputDir, project)
474
+ send({ type: 'author-review', no, review })
475
+ }
476
+ } catch (error) {
477
+ console.warn('[dsh-novel-forge] author review failed:', (error as Error).message)
478
+ }
479
+ }
448
480
  res.end()
449
481
  } catch (error) {
450
482
  chapter.status = 'error'
@@ -1430,6 +1462,51 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
1430
1462
  line.progress = `推进至第 ${body.chapterNo} 章`
1431
1463
  }
1432
1464
  }
1465
+ } else if (op === 'suggest') {
1466
+ try {
1467
+ const suggestions = await suggestPlotlines(ctx, config, project)
1468
+ writeJson(res, 200, { plotlines: project.plotlines, suggestions } satisfies PlotlinesResponse)
1469
+ return
1470
+ } catch (error) {
1471
+ writeJson(res, 500, { error: `AI 建议失败:${(error as Error).message}` })
1472
+ return
1473
+ }
1474
+ } else if (op === 'refresh' && body?.id !== undefined) {
1475
+ const line = project.plotlines.find(l => l.id === body.id)
1476
+ if (line === undefined) {
1477
+ writeJson(res, 404, { error: '剧情线不存在' })
1478
+ return
1479
+ }
1480
+ try {
1481
+ const progress = await refreshPlotlineProgress(ctx, config, project, line)
1482
+ line.progress = progress
1483
+ project.updatedAt = new Date().toISOString()
1484
+ saveProject(config.outputDir, project)
1485
+ writeJson(res, 200, { plotlines: project.plotlines } satisfies PlotlinesResponse)
1486
+ return
1487
+ } catch (error) {
1488
+ writeJson(res, 500, { error: `刷新进度失败:${(error as Error).message}` })
1489
+ return
1490
+ }
1491
+ } else if (op === 'health') {
1492
+ try {
1493
+ const health = await analyzePlotlineHealth(ctx, config, project)
1494
+ writeJson(res, 200, { plotlines: project.plotlines, health } satisfies PlotlinesResponse)
1495
+ return
1496
+ } catch (error) {
1497
+ writeJson(res, 500, { error: `健康检查失败:${(error as Error).message}` })
1498
+ return
1499
+ }
1500
+ } else if (op === 'plan') {
1501
+ try {
1502
+ const health = await analyzePlotlineHealth(ctx, config, project)
1503
+ const plan = await designPlotlinePlan(ctx, config, project, health)
1504
+ writeJson(res, 200, { plotlines: project.plotlines, health, plan } satisfies PlotlinesResponse)
1505
+ return
1506
+ } catch (error) {
1507
+ writeJson(res, 500, { error: `剧情方案生成失败:${(error as Error).message}` })
1508
+ return
1509
+ }
1433
1510
  }
1434
1511
  project.updatedAt = new Date().toISOString()
1435
1512
  saveProject(config.outputDir, project)
@@ -1485,6 +1562,95 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
1485
1562
  },
1486
1563
  }
1487
1564
 
1565
+ // ------------------------------------------------------ author review backfill
1566
+ /** 作者复盘补跑:对已写章节补齐 authorReview(body.chapterNo=单章 JSON,缺省=全书 NDJSON 流)。 */
1567
+ const reviewBackfillRoute: WebRoute = {
1568
+ kind: 'exact',
1569
+ path: NOVEL_API.reviewBackfill,
1570
+ handler: async (req, res) => {
1571
+ if (!guard(req, res, 'POST')) return
1572
+ const config = getConfig()
1573
+ const project = requireProject(res)
1574
+ if (project === undefined) return
1575
+ const body = await readJsonBody<{ chapterNo?: number }>(req)
1576
+
1577
+ /** 对一章执行作者复盘(读取已落盘正文,不改变章节状态/正文)。 */
1578
+ const runOne = async (chapter: ChapterPlan): Promise<AuthorReview> => {
1579
+ const currentBody = readChapterFile(config.outputDir, chapter)
1580
+ if (currentBody === undefined) throw new Error(`章节 ${chapter.no} 的正文文件不存在`)
1581
+ let prevTail = ''
1582
+ if (chapter.no > 1) {
1583
+ const prev = project.chapters.find(c => c.no === chapter.no - 1)
1584
+ if (prev !== undefined) {
1585
+ prevTail = (readChapterFile(config.outputDir, prev) ?? '').replace(/^#.*$/m, '').trim().slice(-600)
1586
+ }
1587
+ }
1588
+ return authorReviewChapter(ctx, config, project, chapter.no, currentBody, prevTail)
1589
+ }
1590
+
1591
+ // 单章:JSON 响应。
1592
+ if (typeof body?.chapterNo === 'number' && body.chapterNo > 0) {
1593
+ const chapter = project.chapters.find(c => c.no === body.chapterNo)
1594
+ if (chapter === undefined) {
1595
+ writeJson(res, 404, { error: `章节 ${body.chapterNo} 不在计划中` })
1596
+ return
1597
+ }
1598
+ if (chapter.status === 'pending') {
1599
+ writeJson(res, 400, { error: '该章尚未生成正文,无法复盘' })
1600
+ return
1601
+ }
1602
+ try {
1603
+ const review = await runOne(chapter)
1604
+ chapter.authorReview = review
1605
+ if (review.advancedLines !== undefined) {
1606
+ autoLinkPlotlines(project, chapter.no, review.advancedLines)
1607
+ }
1608
+ project.updatedAt = new Date().toISOString()
1609
+ saveProject(config.outputDir, project)
1610
+ writeJson(res, 200, { no: chapter.no, review } satisfies { no: number; review: AuthorReview })
1611
+ return
1612
+ } catch (error) {
1613
+ writeJson(res, 500, { error: (error as Error).message })
1614
+ return
1615
+ }
1616
+ }
1617
+
1618
+ // 全书:NDJSON 流式补跑缺失复盘的已写章节。
1619
+ const missing = project.chapters.filter(c => c.status !== 'pending' && c.status !== 'generating' && c.authorReview === undefined)
1620
+ if (missing.length === 0) {
1621
+ writeJson(res, 200, { count: 0 })
1622
+ return
1623
+ }
1624
+ res.writeHead(200, {
1625
+ 'content-type': 'application/x-ndjson; charset=utf-8',
1626
+ 'cache-control': 'no-cache',
1627
+ 'referrer-policy': 'no-referrer',
1628
+ })
1629
+ const send = (frame: JobFrame): void => {
1630
+ res.write(JSON.stringify(frame) + '\n')
1631
+ }
1632
+ let done = 0
1633
+ for (const chapter of missing) {
1634
+ try {
1635
+ const review = await runOne(chapter)
1636
+ chapter.authorReview = review
1637
+ if (review.advancedLines !== undefined) {
1638
+ autoLinkPlotlines(project, chapter.no, review.advancedLines)
1639
+ }
1640
+ done++
1641
+ saveProject(config.outputDir, project)
1642
+ send({ type: 'author-review', no: chapter.no, review })
1643
+ } catch (error) {
1644
+ console.warn(`[dsh-novel-forge] author backfill ch.${chapter.no} failed:`, (error as Error).message)
1645
+ }
1646
+ }
1647
+ project.updatedAt = new Date().toISOString()
1648
+ saveProject(config.outputDir, project)
1649
+ send({ type: 'author-backfill-done', count: done })
1650
+ res.end()
1651
+ },
1652
+ }
1653
+
1488
1654
  // --------------------------------------------------------------- config
1489
1655
  const configRoute: WebRoute = {
1490
1656
  kind: 'exact',
@@ -1561,6 +1727,7 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
1561
1727
  renameRoute,
1562
1728
  plotlinesRoute,
1563
1729
  sensitiveRoute,
1730
+ reviewBackfillRoute,
1564
1731
  configRoute,
1565
1732
  openFolderRoute,
1566
1733
  ]