@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/README.md +4 -1
- package/lib/client.js +3980 -2957
- package/lib/client.js.map +1 -1
- package/lib/index.js +440 -35
- package/lib/index.js.map +1 -1
- package/lib/types/client/api.d.ts +7 -0
- package/lib/types/client/locales.d.ts +4 -2
- package/lib/types/client/panel/NovelPanel.d.ts +1 -1
- package/lib/types/engine.d.ts +13 -1
- package/lib/types/index.d.ts +2 -0
- package/lib/types/protocol.d.ts +65 -3
- package/package.json +1 -1
- package/src/assistant.ts +59 -36
- package/src/client/api.ts +10 -0
- package/src/client/locales.ts +8 -4
- package/src/client/panel/NovelPanel.tsx +704 -154
- package/src/engine.ts +290 -0
- package/src/index.ts +6 -0
- package/src/protocol.ts +63 -3
- package/src/routes.ts +167 -0
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
|
]
|