@waterwx/dsh-novel-forge 2.0.1-alpha → 2.1.0-alpha
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 +97 -2
- package/lib/client.js +3291 -752
- package/lib/index.js +3557 -237
- package/lib/types/assets.d.ts +16 -3
- package/lib/types/assistant.d.ts +4 -0
- package/lib/types/client/api.d.ts +89 -0
- package/lib/types/client/llmLive.d.ts +33 -0
- package/lib/types/client/locales.d.ts +3 -3
- package/lib/types/client/panel/AdaptModeView.d.ts +1 -1
- package/lib/types/client/panel/AssetsTab.d.ts +1 -1
- package/lib/types/client/panel/AssistantTab.d.ts +1 -1
- package/lib/types/client/panel/AuthorAssetsView.d.ts +1 -1
- package/lib/types/client/panel/AuthorHome.d.ts +4 -2
- package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
- package/lib/types/client/panel/BookshelfBar.d.ts +1 -1
- package/lib/types/client/panel/CreateBookView.d.ts +6 -2
- package/lib/types/client/panel/DirectorView.d.ts +7 -0
- package/lib/types/client/panel/FlowGuide.d.ts +1 -1
- package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +1 -1
- package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
- package/lib/types/client/panel/ImportModal.d.ts +1 -1
- package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
- package/lib/types/client/panel/LiveFeedLog.d.ts +5 -0
- package/lib/types/client/panel/MangaRoleLibrary.d.ts +1 -1
- package/lib/types/client/panel/MangaWorkspace.d.ts +1 -1
- package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
- package/lib/types/client/panel/NovelPanel.d.ts +2 -2
- package/lib/types/client/panel/ProgressConsole.d.ts +1 -1
- package/lib/types/client/panel/ProgressHomeView.d.ts +1 -1
- package/lib/types/client/panel/PropLibrary.d.ts +1 -1
- package/lib/types/client/panel/ReaderView.d.ts +1 -1
- package/lib/types/client/panel/RunPanel.d.ts +1 -1
- package/lib/types/client/panel/SceneLibrary.d.ts +1 -1
- package/lib/types/client/panel/SettingsView.d.ts +1 -1
- package/lib/types/client/panel/ShelfView.d.ts +1 -1
- package/lib/types/client/panel/StoryboardTab.d.ts +1 -1
- package/lib/types/client/panel/StyleCard.d.ts +1 -1
- package/lib/types/client/panel/StyleGallery.d.ts +1 -1
- package/lib/types/client/panel/WorldTab.d.ts +1 -1
- package/lib/types/engine.d.ts +29 -1
- package/lib/types/global-assets.d.ts +6 -0
- package/lib/types/index.d.ts +6 -0
- package/lib/types/llm-live.d.ts +44 -0
- package/lib/types/market-radar-scan.d.ts +19 -0
- package/lib/types/market-radar-sources.d.ts +31 -0
- package/lib/types/prompting.d.ts +35 -0
- package/lib/types/protocol.d.ts +364 -8
- package/lib/types/run.d.ts +1 -1
- package/lib/types/src/ai-scan.d.ts +36 -0
- package/lib/types/src/assets.d.ts +37 -0
- package/lib/types/src/assistant.d.ts +45 -0
- package/lib/types/src/author-assets.d.ts +25 -0
- package/lib/types/src/bookshelf.d.ts +42 -0
- package/lib/types/src/client/api.d.ts +234 -0
- package/lib/types/src/client/docx.d.ts +15 -0
- package/lib/types/src/client/index.d.ts +14 -0
- package/lib/types/src/client/locales.d.ts +212 -0
- package/lib/types/src/client/mount.d.ts +9 -0
- package/lib/types/src/client/panel/AdaptModeView.d.ts +5 -0
- package/lib/types/src/client/panel/AssetsTab.d.ts +12 -0
- package/lib/types/src/client/panel/AssistantTab.d.ts +7 -0
- package/lib/types/src/client/panel/AuthorAssetsView.d.ts +4 -0
- package/lib/types/src/client/panel/AuthorHome.d.ts +32 -0
- package/lib/types/src/client/panel/BookshelfBar.d.ts +11 -0
- package/lib/types/src/client/panel/CreateBookView.d.ts +9 -0
- package/lib/types/src/client/panel/FlowGuide.d.ts +22 -0
- package/lib/types/src/client/panel/GlobalAssetLibraryView.d.ts +4 -0
- package/lib/types/src/client/panel/ImportModal.d.ts +7 -0
- package/lib/types/src/client/panel/MangaRoleLibrary.d.ts +20 -0
- package/lib/types/src/client/panel/MangaWorkspace.d.ts +12 -0
- package/lib/types/src/client/panel/ModelManager.d.ts +14 -0
- package/lib/types/src/client/panel/NovelPanel.d.ts +13 -0
- package/lib/types/src/client/panel/ProgressConsole.d.ts +17 -0
- package/lib/types/src/client/panel/ProgressHomeView.d.ts +5 -0
- package/lib/types/src/client/panel/PropLibrary.d.ts +8 -0
- package/lib/types/src/client/panel/ReaderView.d.ts +8 -0
- package/lib/types/src/client/panel/ReasoningSection.d.ts +10 -0
- package/lib/types/src/client/panel/RunPanel.d.ts +5 -0
- package/lib/types/src/client/panel/SceneLibrary.d.ts +16 -0
- package/lib/types/src/client/panel/SettingsView.d.ts +16 -0
- package/lib/types/src/client/panel/ShelfView.d.ts +15 -0
- package/lib/types/src/client/panel/StoryboardTab.d.ts +19 -0
- package/lib/types/src/client/panel/StyleCard.d.ts +8 -0
- package/lib/types/src/client/panel/StyleGallery.d.ts +1 -0
- package/lib/types/src/client/panel/WorldTab.d.ts +10 -0
- package/lib/types/src/client/panel/controller.d.ts +19 -0
- package/lib/types/src/client/panel/helpers.d.ts +18 -0
- package/lib/types/src/client/panel/views.d.ts +77 -0
- package/lib/types/src/client/sidebar-entry.d.ts +13 -0
- package/lib/types/src/client/text.d.ts +9 -0
- package/lib/types/src/comic-presets.d.ts +12 -0
- package/lib/types/src/docx.d.ts +19 -0
- package/lib/types/src/engine.d.ts +402 -0
- package/lib/types/src/index.d.ts +79 -0
- package/lib/types/src/manga-genre-rules.d.ts +22 -0
- package/lib/types/src/novel-context.d.ts +80 -0
- package/lib/types/src/prompt-utils.d.ts +35 -0
- package/lib/types/src/protocol.d.ts +2176 -0
- package/lib/types/src/review-policy.d.ts +97 -0
- package/lib/types/src/routes.d.ts +29 -0
- package/lib/types/src/run.d.ts +32 -0
- package/lib/types/src/shot-language.d.ts +46 -0
- package/lib/types/src/story-beat-language.d.ts +35 -0
- package/lib/types/src/style-library.d.ts +33 -0
- package/package.json +1 -1
- package/src/assets.ts +1774 -879
- package/src/assistant.ts +154 -9
- package/src/client/api.ts +105 -6
- package/src/client/llmLive.ts +101 -0
- package/src/client/locales.ts +4 -4
- package/src/client/panel/AdaptModeView.tsx +18 -3
- package/src/client/panel/AssistantTab.tsx +41 -3
- package/src/client/panel/AuthorHome.tsx +51 -20
- package/src/client/panel/BookAnalysisView.tsx +60 -0
- package/src/client/panel/CreateBookView.tsx +22 -3
- package/src/client/panel/DirectorView.tsx +200 -0
- package/src/client/panel/IdeaInspirationView.tsx +71 -0
- package/src/client/panel/KnowledgeBaseView.tsx +73 -0
- package/src/client/panel/LiveFeedLog.tsx +76 -0
- package/src/client/panel/MarketRadarView.tsx +374 -0
- package/src/client/panel/NovelPanel.tsx +117 -16
- package/src/client/panel/ProgressConsole.tsx +5 -6
- package/src/client/panel/SettingsView.tsx +3 -0
- package/src/client/panel/panel.module.css +2 -3
- package/src/engine.ts +459 -23
- package/src/global-assets.ts +59 -0
- package/src/index.ts +17 -1
- package/src/llm-live.ts +68 -0
- package/src/market-radar-scan.ts +41 -0
- package/src/market-radar-sources.ts +173 -0
- package/src/prompting.ts +159 -0
- package/src/protocol.ts +340 -8
- package/src/routes.ts +579 -59
- package/src/run.ts +15 -8
- package/skills/chapter-batch/SKILL.md +0 -81
package/src/routes.ts
CHANGED
|
@@ -135,12 +135,27 @@ import {
|
|
|
135
135
|
type AdaptMaterializeSaveResponse,
|
|
136
136
|
type AdaptRewriteFrame,
|
|
137
137
|
type PluginUpdateResponse,
|
|
138
|
+
type MarketRadarRequest,
|
|
139
|
+
type MarketRadarBriefRequest,
|
|
140
|
+
type ProductionFoundation,
|
|
141
|
+
type ProgressionMode,
|
|
142
|
+
type GenreNode,
|
|
143
|
+
type KnowledgeDoc,
|
|
144
|
+
type KnowledgeRequest,
|
|
145
|
+
type BookAnalysisRequest,
|
|
146
|
+
type IdeaInspirationRequest,
|
|
147
|
+
type DirectorRequest,
|
|
148
|
+
type DirectorTodo,
|
|
138
149
|
} from './protocol.ts'
|
|
139
150
|
import { readOutlineFromDocx } from './docx.ts'
|
|
140
151
|
import { clearAssistantHistory, loadAssistantHistory, runAssistantTurn } from './assistant.ts'
|
|
141
152
|
import { activateBook, bookshelfSnapshot, createBook, defaultOutputDirFor, importDir, loadBookshelf, removeBook, renameBook, seedBookshelfFromOutputDir } from './bookshelf.ts'
|
|
142
153
|
import { loadAuthorAssets, upsertAuthorAsset, removeAuthorAsset, importDefaultAuthorAssets } from './author-assets.ts'
|
|
143
|
-
import { BUILTIN_ANTI_AI_RULES, BUILTIN_GENRE_LIBRARY, BUILTIN_PLOT_BEATS, BUILTIN_PROGRESSION_MODES, BUILTIN_STYLE_TEMPLATES, emptyProjectAssets } from './assets.ts'
|
|
154
|
+
import { BUILTIN_ANTI_AI_RULES, BUILTIN_GENRE_LIBRARY, BUILTIN_PLOT_BEATS, BUILTIN_PROGRESSION_MODES, BUILTIN_STARTER_STYLE_PROFILES, BUILTIN_STYLE_TEMPLATES, emptyProjectAssets, ensureBuiltinAssets } from './assets.ts'
|
|
155
|
+
import { scanAiFlavor } from './ai-scan.ts'
|
|
156
|
+
import { emitLive, nextSessionId, subscribeLiveFeed } from './llm-live.ts'
|
|
157
|
+
import { scanMarketRanking } from './market-radar-scan.ts'
|
|
158
|
+
import { addGlobalGenre, addGlobalMode, globalGenreLibrary, globalProgressionLibrary } from './global-assets.ts'
|
|
144
159
|
import {
|
|
145
160
|
chapterFileName,
|
|
146
161
|
auditBook,
|
|
@@ -154,6 +169,13 @@ import {
|
|
|
154
169
|
previewBookText,
|
|
155
170
|
extractBible,
|
|
156
171
|
extractStyleAsset,
|
|
172
|
+
extractStyleFormula,
|
|
173
|
+
runMarketRadar,
|
|
174
|
+
runMarketCreativeBrief,
|
|
175
|
+
runBookAnalysis,
|
|
176
|
+
runIdeaInspiration,
|
|
177
|
+
runMarketIdeaInspiration,
|
|
178
|
+
runDirectorAdvice,
|
|
157
179
|
extractWorld,
|
|
158
180
|
generateBlurb,
|
|
159
181
|
generateChapterStream,
|
|
@@ -218,6 +240,15 @@ const MAX_JSON_BODY_BYTES = 64 * 1024 * 1024
|
|
|
218
240
|
/** 包内置风格效果图目录(assets/styles,随 npm 包分发)。 */
|
|
219
241
|
const builtinStyleDir = fileURLToPath(new URL('../assets/styles/', import.meta.url))
|
|
220
242
|
|
|
243
|
+
/** 解析请求级目标书目录:优先按请求携带的 bookId 查书架,否则回退全局 active 书目录。 */
|
|
244
|
+
function resolveOutputDir(config: NovelConfig, bookId?: string): string {
|
|
245
|
+
if (bookId !== undefined && bookId !== '') {
|
|
246
|
+
const book = loadBookshelf().books.find(b => b.id === bookId)
|
|
247
|
+
if (book !== undefined && book.outputDir !== '') return book.outputDir
|
|
248
|
+
}
|
|
249
|
+
return config.outputDir
|
|
250
|
+
}
|
|
251
|
+
|
|
221
252
|
/** Loopback-only fence (mirrors the family plugins' pairing routes). */
|
|
222
253
|
function isLoopbackRequest(request: IncomingMessage): boolean {
|
|
223
254
|
const address = request.socket.remoteAddress
|
|
@@ -321,9 +352,11 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
321
352
|
handler: (req, res) => {
|
|
322
353
|
if (!guard(req, res, 'GET')) return
|
|
323
354
|
const config = getConfig()
|
|
355
|
+
const qurl = new URL(req.url ?? '/', 'http://localhost')
|
|
356
|
+
const outputDir = resolveOutputDir(config, qurl.searchParams.get('bookId') ?? undefined)
|
|
324
357
|
// 书架为空时播种 settings 默认输出目录里的已有项目。
|
|
325
|
-
seedBookshelfFromOutputDir(
|
|
326
|
-
const project = loadProject(
|
|
358
|
+
seedBookshelfFromOutputDir(outputDir)
|
|
359
|
+
const project = loadProject(outputDir)
|
|
327
360
|
if (project !== undefined) {
|
|
328
361
|
// 自动兜底:generating 超过 10 分钟(正常一章 2-5 分钟)视为卡死/中断,
|
|
329
362
|
// 复位为 pending,避免"生成中"状态卡死无法重新生成。
|
|
@@ -338,8 +371,8 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
338
371
|
}
|
|
339
372
|
}
|
|
340
373
|
}
|
|
341
|
-
syncProjectWithDisk(project,
|
|
342
|
-
saveProject(
|
|
374
|
+
syncProjectWithDisk(project, outputDir)
|
|
375
|
+
saveProject(outputDir, project)
|
|
343
376
|
}
|
|
344
377
|
const slim = new URL(req.url ?? '/', 'http://localhost').searchParams.get('slim') === '1'
|
|
345
378
|
let projectPayload: StatusResponse['project']
|
|
@@ -477,9 +510,11 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
477
510
|
path: NOVEL_API.volumes,
|
|
478
511
|
handler: async (req, res) => {
|
|
479
512
|
if (!guard(req, res, 'POST')) return
|
|
480
|
-
const body = await readJsonBody<VolumesRequest>(req)
|
|
481
|
-
const
|
|
482
|
-
const
|
|
513
|
+
const body = await readJsonBody<VolumesRequest & { bookId?: string }>(req)
|
|
514
|
+
const config0 = getConfig()
|
|
515
|
+
const outputDir = resolveOutputDir(config0, body?.bookId)
|
|
516
|
+
const config = { ...config0, outputDir }
|
|
517
|
+
const project = loadProject(outputDir)
|
|
483
518
|
const outline = body?.outline?.trim() !== '' && body?.outline !== undefined
|
|
484
519
|
? body.outline
|
|
485
520
|
: project?.outline
|
|
@@ -493,7 +528,7 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
493
528
|
const next = project ?? createProject(outline)
|
|
494
529
|
next.volumes = volumes
|
|
495
530
|
next.updatedAt = now
|
|
496
|
-
saveProject(
|
|
531
|
+
saveProject(outputDir, next)
|
|
497
532
|
const response: VolumesResponse = { volumes }
|
|
498
533
|
writeJson(res, 200, response)
|
|
499
534
|
} catch (error) {
|
|
@@ -508,9 +543,11 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
508
543
|
path: NOVEL_API.plan,
|
|
509
544
|
handler: async (req, res) => {
|
|
510
545
|
if (!guard(req, res, 'POST')) return
|
|
511
|
-
const body = await readJsonBody<PlanRequest>(req)
|
|
512
|
-
const
|
|
513
|
-
const
|
|
546
|
+
const body = await readJsonBody<PlanRequest & { bookId?: string }>(req)
|
|
547
|
+
const config0 = getConfig()
|
|
548
|
+
const outputDir = resolveOutputDir(config0, body?.bookId)
|
|
549
|
+
const config = { ...config0, outputDir }
|
|
550
|
+
const project = loadProject(outputDir)
|
|
514
551
|
const outline = body?.outline?.trim() !== '' && body?.outline !== undefined
|
|
515
552
|
? body.outline
|
|
516
553
|
: project?.outline
|
|
@@ -546,10 +583,15 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
546
583
|
path: NOVEL_API.generate,
|
|
547
584
|
handler: async (req, res) => {
|
|
548
585
|
if (!guard(req, res, 'POST')) return
|
|
549
|
-
const
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
const
|
|
586
|
+
const config0 = getConfig()
|
|
587
|
+
const body = await readJsonBody<{ chapterNo?: number; skipReview?: boolean; bookId?: string }>(req)
|
|
588
|
+
const outputDir = resolveOutputDir(config0, body?.bookId)
|
|
589
|
+
const config = { ...config0, outputDir }
|
|
590
|
+
const project = loadProject(outputDir)
|
|
591
|
+
if (project === undefined) {
|
|
592
|
+
writeJson(res, 400, { error: '输出目录中没有项目' })
|
|
593
|
+
return
|
|
594
|
+
}
|
|
553
595
|
const rawNo = body?.chapterNo
|
|
554
596
|
if (!Number.isInteger(rawNo) || rawNo === undefined || rawNo < 1) {
|
|
555
597
|
writeJson(res, 400, { error: 'chapterNo 须为正整数' })
|
|
@@ -584,15 +626,23 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
584
626
|
res.write(JSON.stringify(frame) + '\n')
|
|
585
627
|
}
|
|
586
628
|
|
|
629
|
+
const liveSession = nextSessionId()
|
|
630
|
+
emitLive({ type: 'session_started', sessionId: liveSession, label: '正文生成', model: config.generateModel || config.model, at: new Date().toISOString(), context: { interactionId: liveSession, taskId: `ch-${no}` } })
|
|
631
|
+
emitLive({ type: 'phase_changed', sessionId: liveSession, phase: 'streaming', phaseMessage: '模型正在返回正文', at: new Date().toISOString() })
|
|
632
|
+
|
|
587
633
|
try {
|
|
588
634
|
send({ type: 'start', no, title: chapter.title })
|
|
635
|
+
let genChars = 0
|
|
589
636
|
for await (const step of generateChapterStream(ctx, config, project, config.outputDir, no)) {
|
|
590
637
|
if (step.frame === 'delta') {
|
|
591
638
|
send({ type: 'delta', text: step.text })
|
|
639
|
+
genChars += step.text.length
|
|
640
|
+
emitLive({ type: 'output_delta', sessionId: liveSession, content: step.text, totalChars: genChars, at: new Date().toISOString() })
|
|
592
641
|
} else if (step.frame === 'done') {
|
|
593
642
|
send({ type: 'done', no, file: step.file, chars: step.chars, title: chapter.title, warn: step.warn })
|
|
594
643
|
}
|
|
595
644
|
}
|
|
645
|
+
emitLive({ type: 'session_completed', sessionId: liveSession, totalChars: genChars, preview: '', at: new Date().toISOString(), phase: 'completed' })
|
|
596
646
|
// Auto pipeline: summary + facts(一次调用)-> review(unless skipped)。
|
|
597
647
|
try {
|
|
598
648
|
await summarizeAndExtractFacts(ctx, config, project, config.outputDir, no)
|
|
@@ -662,17 +712,22 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
662
712
|
path: NOVEL_API.review,
|
|
663
713
|
handler: async (req, res) => {
|
|
664
714
|
if (!guard(req, res, 'POST')) return
|
|
665
|
-
const
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
const
|
|
715
|
+
const config0 = getConfig()
|
|
716
|
+
const body = await readJsonBody<ReviewRequest & { bookId?: string }>(req)
|
|
717
|
+
const outputDir = resolveOutputDir(config0, body?.bookId)
|
|
718
|
+
const config = { ...config0, outputDir }
|
|
719
|
+
const project = loadProject(outputDir)
|
|
720
|
+
if (project === undefined) {
|
|
721
|
+
writeJson(res, 400, { error: '输出目录中没有项目' })
|
|
722
|
+
return
|
|
723
|
+
}
|
|
669
724
|
if (!Number.isInteger(body?.chapterNo)) {
|
|
670
725
|
writeJson(res, 400, { error: 'chapterNo 须为正整数' })
|
|
671
726
|
return
|
|
672
727
|
}
|
|
673
728
|
const no = body!.chapterNo!
|
|
674
729
|
try {
|
|
675
|
-
const report = await reviewChapter(ctx, config, project,
|
|
730
|
+
const report = await reviewChapter(ctx, config, project, outputDir, no)
|
|
676
731
|
writeJson(res, 200, { report })
|
|
677
732
|
} catch (error) {
|
|
678
733
|
writeJson(res, 500, { error: (error as Error).message })
|
|
@@ -686,10 +741,15 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
686
741
|
path: NOVEL_API.rewrite,
|
|
687
742
|
handler: async (req, res) => {
|
|
688
743
|
if (!guard(req, res, 'POST')) return
|
|
689
|
-
const
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
const
|
|
744
|
+
const config0 = getConfig()
|
|
745
|
+
const body = await readJsonBody<RewriteRequest & { bookId?: string }>(req)
|
|
746
|
+
const outputDir = resolveOutputDir(config0, body?.bookId)
|
|
747
|
+
const config = { ...config0, outputDir }
|
|
748
|
+
const project = loadProject(outputDir)
|
|
749
|
+
if (project === undefined) {
|
|
750
|
+
writeJson(res, 400, { error: '输出目录中没有项目' })
|
|
751
|
+
return
|
|
752
|
+
}
|
|
693
753
|
if (!Number.isInteger(body?.chapterNo)) {
|
|
694
754
|
writeJson(res, 400, { error: 'chapterNo 须为正整数' })
|
|
695
755
|
return
|
|
@@ -725,10 +785,15 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
725
785
|
path: NOVEL_API.polish,
|
|
726
786
|
handler: async (req, res) => {
|
|
727
787
|
if (!guard(req, res, 'POST')) return
|
|
728
|
-
const
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
const
|
|
788
|
+
const config0 = getConfig()
|
|
789
|
+
const body = await readJsonBody<PolishRequest & { bookId?: string }>(req)
|
|
790
|
+
const outputDir = resolveOutputDir(config0, body?.bookId)
|
|
791
|
+
const config = { ...config0, outputDir }
|
|
792
|
+
const project = loadProject(outputDir)
|
|
793
|
+
if (project === undefined) {
|
|
794
|
+
writeJson(res, 400, { error: '输出目录中没有项目' })
|
|
795
|
+
return
|
|
796
|
+
}
|
|
732
797
|
if (!Number.isInteger(body?.chapterNo)) {
|
|
733
798
|
writeJson(res, 400, { error: 'chapterNo 须为正整数' })
|
|
734
799
|
return
|
|
@@ -865,10 +930,15 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
865
930
|
path: NOVEL_API.foreshadow,
|
|
866
931
|
handler: async (req, res) => {
|
|
867
932
|
if (!guard(req, res, 'POST')) return
|
|
868
|
-
const
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
const
|
|
933
|
+
const config0 = getConfig()
|
|
934
|
+
const body = await readJsonBody<ForeshadowRequest & { bookId?: string }>(req)
|
|
935
|
+
const outputDir = resolveOutputDir(config0, body?.bookId)
|
|
936
|
+
const config = { ...config0, outputDir }
|
|
937
|
+
const project = loadProject(outputDir)
|
|
938
|
+
if (project === undefined) {
|
|
939
|
+
writeJson(res, 400, { error: '输出目录中没有项目' })
|
|
940
|
+
return
|
|
941
|
+
}
|
|
872
942
|
try {
|
|
873
943
|
if (body?.suggest === true) {
|
|
874
944
|
// AI suggestion pass: create several foreshadows from the outline.
|
|
@@ -922,10 +992,15 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
922
992
|
path: NOVEL_API.exportBook,
|
|
923
993
|
handler: async (req, res) => {
|
|
924
994
|
if (!guard(req, res, 'POST')) return
|
|
925
|
-
const
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
const
|
|
995
|
+
const config0 = getConfig()
|
|
996
|
+
const body = await readJsonBody<ExportRequest & { bookId?: string }>(req)
|
|
997
|
+
const outputDir = resolveOutputDir(config0, body?.bookId)
|
|
998
|
+
const config = { ...config0, outputDir }
|
|
999
|
+
const project = loadProject(outputDir)
|
|
1000
|
+
if (project === undefined) {
|
|
1001
|
+
writeJson(res, 400, { error: '输出目录中没有项目' })
|
|
1002
|
+
return
|
|
1003
|
+
}
|
|
929
1004
|
const format = body?.format === 'md' ? 'md' : 'txt'
|
|
930
1005
|
try {
|
|
931
1006
|
const result = exportBook(config.outputDir, project, format)
|
|
@@ -944,9 +1019,13 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
944
1019
|
handler: async (req, res) => {
|
|
945
1020
|
if (!guard(req, res, 'GET')) return
|
|
946
1021
|
const config = getConfig()
|
|
947
|
-
const project = requireProject(res)
|
|
948
|
-
if (project === undefined) return
|
|
949
1022
|
const url = new URL(req.url ?? '/', 'http://localhost')
|
|
1023
|
+
const outputDir = resolveOutputDir(config, url.searchParams.get('bookId') ?? undefined)
|
|
1024
|
+
const project = loadProject(outputDir)
|
|
1025
|
+
if (project === undefined) {
|
|
1026
|
+
writeJson(res, 400, { error: '输出目录中没有项目' })
|
|
1027
|
+
return
|
|
1028
|
+
}
|
|
950
1029
|
const rawNo = Number(url.searchParams.get('no') ?? '0')
|
|
951
1030
|
if (!Number.isInteger(rawNo) || rawNo < 1) {
|
|
952
1031
|
writeJson(res, 400, { error: 'no 须为正整数' })
|
|
@@ -957,7 +1036,7 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
957
1036
|
writeJson(res, 404, { error: `章节 ${rawNo} 不在计划中` })
|
|
958
1037
|
return
|
|
959
1038
|
}
|
|
960
|
-
const markdown = readChapterFile(
|
|
1039
|
+
const markdown = readChapterFile(outputDir, chapter)
|
|
961
1040
|
if (markdown === undefined) {
|
|
962
1041
|
writeJson(res, 404, { error: `章节 ${rawNo} 尚未生成` })
|
|
963
1042
|
return
|
|
@@ -1052,9 +1131,13 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
1052
1131
|
handler: async (req, res) => {
|
|
1053
1132
|
if (!guard(req, res, 'POST')) return
|
|
1054
1133
|
const config = getConfig()
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
const
|
|
1134
|
+
const body = await readJsonBody<AssistantRequest & { bookId?: string }>(req)
|
|
1135
|
+
const outputDir = resolveOutputDir(config, body?.bookId)
|
|
1136
|
+
const project = loadProject(outputDir)
|
|
1137
|
+
if (project === undefined) {
|
|
1138
|
+
writeJson(res, 400, { error: '输出目录中没有项目' })
|
|
1139
|
+
return
|
|
1140
|
+
}
|
|
1058
1141
|
const message = body?.message?.trim()
|
|
1059
1142
|
if (message === undefined || message === '') {
|
|
1060
1143
|
writeJson(res, 400, { error: '消息不能为空' })
|
|
@@ -1068,10 +1151,11 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
1068
1151
|
})
|
|
1069
1152
|
const send = (frame: AssistantFrame): void => { res.write(JSON.stringify(frame) + '\n') }
|
|
1070
1153
|
try {
|
|
1071
|
-
for await (const step of runAssistantTurn(ctx, config, project,
|
|
1154
|
+
for await (const step of runAssistantTurn(ctx, config, project, outputDir, message)) {
|
|
1072
1155
|
if (step.frame === 'delta') send({ type: 'delta', text: step.text })
|
|
1073
1156
|
else if (step.frame === 'tool') send({ type: 'tool', name: step.name, status: step.status, detail: step.detail })
|
|
1074
1157
|
else if (step.frame === 'toolDelta') send({ type: 'toolDelta', name: step.name, text: step.text })
|
|
1158
|
+
else if (step.frame === 'toolResult') send({ type: 'toolResult', name: step.name, text: step.text })
|
|
1075
1159
|
}
|
|
1076
1160
|
send({ type: 'done' })
|
|
1077
1161
|
res.end()
|
|
@@ -1091,7 +1175,9 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
1091
1175
|
handler: (req, res) => {
|
|
1092
1176
|
if (!guard(req, res, 'GET')) return
|
|
1093
1177
|
const config = getConfig()
|
|
1094
|
-
const
|
|
1178
|
+
const qurl = new URL(req.url ?? '/', 'http://localhost')
|
|
1179
|
+
const outputDir = resolveOutputDir(config, qurl.searchParams.get('bookId') ?? undefined)
|
|
1180
|
+
const response: AssistantHistoryResponse = { messages: loadAssistantHistory(outputDir) }
|
|
1095
1181
|
writeJson(res, 200, response)
|
|
1096
1182
|
},
|
|
1097
1183
|
}
|
|
@@ -1101,10 +1187,12 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
1101
1187
|
const assistantClearRoute: WebRoute = {
|
|
1102
1188
|
kind: 'exact',
|
|
1103
1189
|
path: NOVEL_API.assistantClear,
|
|
1104
|
-
handler: (req, res) => {
|
|
1190
|
+
handler: async (req, res) => {
|
|
1105
1191
|
if (!guard(req, res, 'POST')) return
|
|
1106
1192
|
const config = getConfig()
|
|
1107
|
-
|
|
1193
|
+
const body = await readJsonBody<{ bookId?: string }>(req)
|
|
1194
|
+
const outputDir = resolveOutputDir(config, body?.bookId)
|
|
1195
|
+
clearAssistantHistory(outputDir)
|
|
1108
1196
|
writeJson(res, 200, { ok: true })
|
|
1109
1197
|
},
|
|
1110
1198
|
}
|
|
@@ -1125,10 +1213,21 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
1125
1213
|
return
|
|
1126
1214
|
}
|
|
1127
1215
|
const config = getConfig()
|
|
1128
|
-
|
|
1129
|
-
|
|
1216
|
+
let postBody: (AssetsPatch & { bookId?: string }) | undefined
|
|
1217
|
+
if (req.method === 'POST') {
|
|
1218
|
+
postBody = await readJsonBody<AssetsPatch & { bookId?: string }>(req)
|
|
1219
|
+
if (postBody === undefined) {
|
|
1220
|
+
writeJson(res, 400, { error: '无效的 JSON' })
|
|
1221
|
+
return
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
const qurl = new URL(req.url ?? '/', 'http://localhost')
|
|
1225
|
+
const queryBookId = qurl.searchParams.get('bookId') ?? undefined
|
|
1226
|
+
const outputDir = resolveOutputDir(config, postBody?.bookId ?? queryBookId)
|
|
1227
|
+
const project = loadProject(outputDir)
|
|
1228
|
+
const projectAssets = ensureBuiltinAssets(project?.assets ?? emptyProjectAssets())
|
|
1130
1229
|
if (req.method === 'POST') {
|
|
1131
|
-
const body =
|
|
1230
|
+
const body = postBody
|
|
1132
1231
|
if (body === undefined) {
|
|
1133
1232
|
writeJson(res, 400, { error: '无效的 JSON' })
|
|
1134
1233
|
return
|
|
@@ -1145,14 +1244,15 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
1145
1244
|
projectAssets.updatedAt = new Date().toISOString()
|
|
1146
1245
|
project.assets = projectAssets
|
|
1147
1246
|
project.updatedAt = new Date().toISOString()
|
|
1148
|
-
saveProject(
|
|
1247
|
+
saveProject(outputDir, project)
|
|
1149
1248
|
}
|
|
1150
1249
|
const response: AssetsResponse = {
|
|
1151
1250
|
projectAssets,
|
|
1152
|
-
genreLibrary: BUILTIN_GENRE_LIBRARY,
|
|
1251
|
+
genreLibrary: [...BUILTIN_GENRE_LIBRARY, ...globalGenreLibrary()],
|
|
1153
1252
|
antiAiLibrary: BUILTIN_ANTI_AI_RULES,
|
|
1154
1253
|
styleTemplates: BUILTIN_STYLE_TEMPLATES,
|
|
1155
|
-
progressionLibrary: BUILTIN_PROGRESSION_MODES,
|
|
1254
|
+
progressionLibrary: [...BUILTIN_PROGRESSION_MODES, ...globalProgressionLibrary()],
|
|
1255
|
+
starterStyleProfiles: BUILTIN_STARTER_STYLE_PROFILES,
|
|
1156
1256
|
plotBeatLibrary: BUILTIN_PLOT_BEATS,
|
|
1157
1257
|
}
|
|
1158
1258
|
writeJson(res, 200, response)
|
|
@@ -1197,6 +1297,404 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
1197
1297
|
},
|
|
1198
1298
|
}
|
|
1199
1299
|
|
|
1300
|
+
// ----------------------------------------------------------- style-formula
|
|
1301
|
+
const styleFormulaRoute: WebRoute = {
|
|
1302
|
+
kind: 'exact',
|
|
1303
|
+
path: NOVEL_API.styleFormula,
|
|
1304
|
+
handler: async (req, res) => {
|
|
1305
|
+
if (!guard(req, res, 'POST')) return
|
|
1306
|
+
const config = getConfig()
|
|
1307
|
+
const project = loadProject(config.outputDir)
|
|
1308
|
+
const body = await readJsonBody<StyleEngineRequest & { depth?: string }>(req)
|
|
1309
|
+
const sample = body?.sampleText?.trim()
|
|
1310
|
+
if (sample === undefined || sample.length < 50) {
|
|
1311
|
+
writeJson(res, 400, { error: '样本文本过短(<50 字符),请粘贴一段能代表目标风格的文字' })
|
|
1312
|
+
return
|
|
1313
|
+
}
|
|
1314
|
+
const depth = (['basic', 'standard', 'deep'] as const).includes(body?.depth as never)
|
|
1315
|
+
? body!.depth! as 'basic' | 'standard' | 'deep'
|
|
1316
|
+
: 'standard'
|
|
1317
|
+
try {
|
|
1318
|
+
const formula = await extractStyleFormula(ctx, config, sample, depth)
|
|
1319
|
+
const key = `sf-${Date.now().toString(36)}`
|
|
1320
|
+
const styleFormula = { key, ...formula, depth, createdAt: new Date().toISOString() }
|
|
1321
|
+
if (project !== undefined) {
|
|
1322
|
+
project.assets ??= emptyProjectAssets()
|
|
1323
|
+
project.assets.styleFormulas ??= []
|
|
1324
|
+
project.assets.styleFormulas.push(styleFormula)
|
|
1325
|
+
project.assets.updatedAt = new Date().toISOString()
|
|
1326
|
+
project.updatedAt = new Date().toISOString()
|
|
1327
|
+
saveProject(config.outputDir, project)
|
|
1328
|
+
}
|
|
1329
|
+
writeJson(res, 200, { styleFormula })
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
writeJson(res, 500, { error: (error as Error).message })
|
|
1332
|
+
}
|
|
1333
|
+
},
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// ----------------------------------------------------------- style-detect
|
|
1337
|
+
/** 正文实时 AI 味检测(本地确定性扫描,免 LLM)。 */
|
|
1338
|
+
const styleDetectRoute: WebRoute = {
|
|
1339
|
+
kind: 'exact',
|
|
1340
|
+
path: NOVEL_API.styleDetect,
|
|
1341
|
+
handler: async (req, res) => {
|
|
1342
|
+
if (!guard(req, res, 'POST')) return
|
|
1343
|
+
const body = await readJsonBody<{ text?: string }>(req)
|
|
1344
|
+
const text = body?.text?.trim() ?? ''
|
|
1345
|
+
if (text.length < 20) {
|
|
1346
|
+
writeJson(res, 400, { error: '文本过短(<20 字符)' })
|
|
1347
|
+
return
|
|
1348
|
+
}
|
|
1349
|
+
const scan = scanAiFlavor(text.slice(0, 20000))
|
|
1350
|
+
writeJson(res, 200, { scan })
|
|
1351
|
+
},
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// --------------------------------------------------------- market-radar
|
|
1355
|
+
/** 热门题材雷达:输入平台/题材/榜单文本 → 信号 + 生产底座 + 开书创意。 */
|
|
1356
|
+
const marketRadarRoute: WebRoute = {
|
|
1357
|
+
kind: 'exact',
|
|
1358
|
+
path: NOVEL_API.marketRadar,
|
|
1359
|
+
handler: async (req, res) => {
|
|
1360
|
+
if (!guard(req, res, 'POST')) return
|
|
1361
|
+
const config = getConfig()
|
|
1362
|
+
const body = await readJsonBody<MarketRadarRequest>(req)
|
|
1363
|
+
try {
|
|
1364
|
+
const result = await runMarketRadar(ctx, config, body ?? {})
|
|
1365
|
+
writeJson(res, 200, { result })
|
|
1366
|
+
} catch (error) {
|
|
1367
|
+
writeJson(res, 500, { error: (error as Error).message })
|
|
1368
|
+
}
|
|
1369
|
+
},
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// ------------------------------------------------- market-radar scan
|
|
1373
|
+
/** 真实榜单扫榜:抓取公开榜单(容错),返回分组候选。 */
|
|
1374
|
+
const marketRadarScanRoute: WebRoute = {
|
|
1375
|
+
kind: 'exact',
|
|
1376
|
+
path: NOVEL_API.marketRadarScan,
|
|
1377
|
+
handler: async (req, res) => {
|
|
1378
|
+
if (!guard(req, res, 'POST')) return
|
|
1379
|
+
const body = await readJsonBody<{ platforms?: string[] }>(req)
|
|
1380
|
+
try {
|
|
1381
|
+
const result = await scanMarketRanking(body?.platforms)
|
|
1382
|
+
writeJson(res, 200, { result })
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
writeJson(res, 500, { error: (error as Error).message })
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// ------------------------------------------------- market-radar apply
|
|
1390
|
+
/** 把雷达生产底座一键应用到某本书(写入项目资产/开书定盘,供后续规划与生成遵守)。 */
|
|
1391
|
+
const marketRadarApplyRoute: WebRoute = {
|
|
1392
|
+
kind: 'exact',
|
|
1393
|
+
path: NOVEL_API.marketRadarApply,
|
|
1394
|
+
handler: async (req, res) => {
|
|
1395
|
+
if (!guard(req, res, 'POST')) return
|
|
1396
|
+
const body = await readJsonBody<{ bookId?: string; foundation?: ProductionFoundation }>(req)
|
|
1397
|
+
const pf = body?.foundation
|
|
1398
|
+
if (pf === undefined) {
|
|
1399
|
+
writeJson(res, 400, { error: '缺少 foundation' })
|
|
1400
|
+
return
|
|
1401
|
+
}
|
|
1402
|
+
const config = getConfig()
|
|
1403
|
+
const outputDir = resolveOutputDir(config, body?.bookId)
|
|
1404
|
+
const project = loadProject(outputDir)
|
|
1405
|
+
if (project === undefined) {
|
|
1406
|
+
writeJson(res, 400, { error: '输出目录中没有项目' })
|
|
1407
|
+
return
|
|
1408
|
+
}
|
|
1409
|
+
const genreBuiltin = BUILTIN_GENRE_LIBRARY.find(g => g.name === (pf.genre.existingId || pf.genre.name))
|
|
1410
|
+
const genreNode: GenreNode = genreBuiltin !== undefined
|
|
1411
|
+
? { ...genreBuiltin }
|
|
1412
|
+
: { name: pf.genre.name.slice(0, 40), description: pf.genre.description, template: pf.genre.template, children: [] }
|
|
1413
|
+
const findMode = (m: { existingId?: string; name: string }): ProgressionMode | undefined => BUILTIN_PROGRESSION_MODES.find(x => x.name === (m.existingId || m.name))
|
|
1414
|
+
const primary = findMode(pf.primaryStoryMode) ?? {
|
|
1415
|
+
name: pf.primaryStoryMode.name.slice(0, 40),
|
|
1416
|
+
driver: pf.primaryStoryMode.driver,
|
|
1417
|
+
readerExpectation: pf.primaryStoryMode.readerExpectation,
|
|
1418
|
+
payoffs: [],
|
|
1419
|
+
risks: [],
|
|
1420
|
+
primary: true,
|
|
1421
|
+
}
|
|
1422
|
+
const aux: ProgressionMode[] = []
|
|
1423
|
+
if (pf.secondaryStoryMode !== undefined) {
|
|
1424
|
+
const sec = findMode(pf.secondaryStoryMode) ?? {
|
|
1425
|
+
name: pf.secondaryStoryMode.name.slice(0, 40),
|
|
1426
|
+
driver: pf.secondaryStoryMode.driver,
|
|
1427
|
+
readerExpectation: pf.secondaryStoryMode.readerExpectation,
|
|
1428
|
+
payoffs: [],
|
|
1429
|
+
risks: [],
|
|
1430
|
+
primary: false,
|
|
1431
|
+
}
|
|
1432
|
+
aux.push(sec)
|
|
1433
|
+
}
|
|
1434
|
+
project.assets ??= emptyProjectAssets()
|
|
1435
|
+
project.assets.genre = genreNode
|
|
1436
|
+
project.assets.primaryProgression = primary
|
|
1437
|
+
project.assets.auxiliaryProgressions = aux
|
|
1438
|
+
project.bookContract = {
|
|
1439
|
+
promise: pf.genre.description.slice(0, 120),
|
|
1440
|
+
primaryModeName: pf.primaryStoryMode.name,
|
|
1441
|
+
secondaryModeNames: pf.secondaryStoryMode !== undefined ? [pf.secondaryStoryMode.name] : [],
|
|
1442
|
+
tone: pf.genre.template,
|
|
1443
|
+
targetPlatform: undefined,
|
|
1444
|
+
}
|
|
1445
|
+
project.updatedAt = new Date().toISOString()
|
|
1446
|
+
saveProject(outputDir, project)
|
|
1447
|
+
writeJson(res, 200, { ok: true, bookName: project.bookName })
|
|
1448
|
+
},
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// ------------------------------------------------- market-radar sync
|
|
1452
|
+
/** 把雷达生产底座里的「新资产」同步进全局资源库(跨书复用;已有名字跳过)。 */
|
|
1453
|
+
const marketRadarSyncRoute: WebRoute = {
|
|
1454
|
+
kind: 'exact',
|
|
1455
|
+
path: NOVEL_API.marketRadarSync,
|
|
1456
|
+
handler: async (req, res) => {
|
|
1457
|
+
if (!guard(req, res, 'POST')) return
|
|
1458
|
+
const body = await readJsonBody<{ foundation?: ProductionFoundation }>(req)
|
|
1459
|
+
const pf = body?.foundation
|
|
1460
|
+
if (pf === undefined) {
|
|
1461
|
+
writeJson(res, 400, { error: '缺少 foundation' })
|
|
1462
|
+
return
|
|
1463
|
+
}
|
|
1464
|
+
const synced = { genre: false, primaryMode: false, secondaryMode: false }
|
|
1465
|
+
if (pf.genre.existingId === undefined && pf.genre.name !== '') {
|
|
1466
|
+
synced.genre = addGlobalGenre({ name: pf.genre.name.slice(0, 40), description: pf.genre.description, template: pf.genre.template, children: [] })
|
|
1467
|
+
}
|
|
1468
|
+
if (pf.primaryStoryMode.existingId === undefined && pf.primaryStoryMode.name !== '') {
|
|
1469
|
+
synced.primaryMode = addGlobalMode({ name: pf.primaryStoryMode.name.slice(0, 40), driver: pf.primaryStoryMode.driver, readerExpectation: pf.primaryStoryMode.readerExpectation, payoffs: [], risks: [], primary: true })
|
|
1470
|
+
}
|
|
1471
|
+
if (pf.secondaryStoryMode !== undefined && pf.secondaryStoryMode !== null && pf.secondaryStoryMode.existingId === undefined && pf.secondaryStoryMode.name !== '') {
|
|
1472
|
+
synced.secondaryMode = addGlobalMode({ name: pf.secondaryStoryMode.name.slice(0, 40), driver: pf.secondaryStoryMode.driver, readerExpectation: pf.secondaryStoryMode.readerExpectation, payoffs: [], risks: [], primary: false })
|
|
1473
|
+
}
|
|
1474
|
+
writeJson(res, 200, { ok: true, synced })
|
|
1475
|
+
},
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// ------------------------------------------------- market-radar brief
|
|
1479
|
+
/** 开书创意简报:用选中的市场信号 + 影响模式生成。 */
|
|
1480
|
+
const marketRadarBriefRoute: WebRoute = {
|
|
1481
|
+
kind: 'exact',
|
|
1482
|
+
path: NOVEL_API.marketRadarBrief,
|
|
1483
|
+
handler: async (req, res) => {
|
|
1484
|
+
if (!guard(req, res, 'POST')) return
|
|
1485
|
+
const config = getConfig()
|
|
1486
|
+
const body = await readJsonBody<MarketRadarBriefRequest>(req)
|
|
1487
|
+
if (body === undefined || body.influenceMode === undefined) {
|
|
1488
|
+
writeJson(res, 400, { error: '缺少 influenceMode' })
|
|
1489
|
+
return
|
|
1490
|
+
}
|
|
1491
|
+
try {
|
|
1492
|
+
const creativeBrief = await runMarketCreativeBrief(ctx, config, body)
|
|
1493
|
+
writeJson(res, 200, { creativeBrief })
|
|
1494
|
+
} catch (error) {
|
|
1495
|
+
writeJson(res, 500, { error: (error as Error).message })
|
|
1496
|
+
}
|
|
1497
|
+
},
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// ------------------------------------------------------------- knowledge
|
|
1501
|
+
/** 书内知识库:GET 读取 / POST 增删改(按书路由)。 */
|
|
1502
|
+
const knowledgeRoute: WebRoute = {
|
|
1503
|
+
kind: 'exact',
|
|
1504
|
+
path: NOVEL_API.knowledge,
|
|
1505
|
+
handler: async (req, res) => {
|
|
1506
|
+
if (req.method === 'POST') {
|
|
1507
|
+
if (!guard(req, res, 'POST')) return
|
|
1508
|
+
} else {
|
|
1509
|
+
if (!guard(req, res, 'GET')) return
|
|
1510
|
+
}
|
|
1511
|
+
const config = getConfig()
|
|
1512
|
+
const qurl = new URL(req.url ?? '/', 'http://localhost')
|
|
1513
|
+
const body = req.method === 'POST' ? await readJsonBody<KnowledgeRequest & { bookId?: string }>(req) : undefined
|
|
1514
|
+
const outputDir = resolveOutputDir(config, body?.bookId ?? qurl.searchParams.get('bookId') ?? undefined)
|
|
1515
|
+
const project = loadProject(outputDir)
|
|
1516
|
+
if (project === undefined) {
|
|
1517
|
+
writeJson(res, 400, { error: '输出目录中没有项目' })
|
|
1518
|
+
return
|
|
1519
|
+
}
|
|
1520
|
+
project.knowledgeDocs ??= []
|
|
1521
|
+
if (req.method === 'POST' && body !== undefined) {
|
|
1522
|
+
if (body.action === 'add' && body.doc !== undefined) {
|
|
1523
|
+
project.knowledgeDocs.push({ id: body.doc.id ?? `kd-${Date.now().toString(36)}`, title: body.doc.title, content: body.doc.content, updatedAt: new Date().toISOString() })
|
|
1524
|
+
} else if (body.action === 'remove' && body.id !== undefined) {
|
|
1525
|
+
project.knowledgeDocs = project.knowledgeDocs.filter(d => d.id !== body.id)
|
|
1526
|
+
} else if (body.action === 'replace' && body.doc !== undefined) {
|
|
1527
|
+
project.knowledgeDocs = project.knowledgeDocs.map(d => d.id === body.doc!.id ? { ...d, title: body.doc!.title, content: body.doc!.content, updatedAt: new Date().toISOString() } : d)
|
|
1528
|
+
}
|
|
1529
|
+
project.updatedAt = new Date().toISOString()
|
|
1530
|
+
saveProject(outputDir, project)
|
|
1531
|
+
}
|
|
1532
|
+
writeJson(res, 200, { docs: project.knowledgeDocs })
|
|
1533
|
+
},
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// ---------------------------------------------------------- book-analysis
|
|
1537
|
+
/** 书分析/拆书:输入文本 → 卖点/结构/可借鉴点/风险。 */
|
|
1538
|
+
const bookAnalysisRoute: WebRoute = {
|
|
1539
|
+
kind: 'exact',
|
|
1540
|
+
path: NOVEL_API.bookAnalysis,
|
|
1541
|
+
handler: async (req, res) => {
|
|
1542
|
+
if (!guard(req, res, 'POST')) return
|
|
1543
|
+
const config = getConfig()
|
|
1544
|
+
const body = await readJsonBody<BookAnalysisRequest>(req)
|
|
1545
|
+
if (body === undefined || (body.text ?? '').trim().length < 50) {
|
|
1546
|
+
writeJson(res, 400, { error: '文本过短(<50 字符)' })
|
|
1547
|
+
return
|
|
1548
|
+
}
|
|
1549
|
+
try {
|
|
1550
|
+
const result = await runBookAnalysis(ctx, config, body)
|
|
1551
|
+
writeJson(res, 200, { result })
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
writeJson(res, 500, { error: (error as Error).message })
|
|
1554
|
+
}
|
|
1555
|
+
},
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// ---------------------------------------------------------- idea-inspiration
|
|
1559
|
+
/** 创意灵感:一句话/题材 → 多方向开书灵感。 */
|
|
1560
|
+
const ideaInspirationRoute: WebRoute = {
|
|
1561
|
+
kind: 'exact',
|
|
1562
|
+
path: NOVEL_API.ideaInspiration,
|
|
1563
|
+
handler: async (req, res) => {
|
|
1564
|
+
if (!guard(req, res, 'POST')) return
|
|
1565
|
+
const config = getConfig()
|
|
1566
|
+
const body = await readJsonBody<IdeaInspirationRequest>(req)
|
|
1567
|
+
if (body === undefined || (body.idea ?? '').trim().length < 2) {
|
|
1568
|
+
writeJson(res, 400, { error: '请填写你的方向/一句话想法' })
|
|
1569
|
+
return
|
|
1570
|
+
}
|
|
1571
|
+
try {
|
|
1572
|
+
const result = await runIdeaInspiration(ctx, config, body)
|
|
1573
|
+
writeJson(res, 200, { result })
|
|
1574
|
+
} catch (error) {
|
|
1575
|
+
writeJson(res, 500, { error: (error as Error).message })
|
|
1576
|
+
}
|
|
1577
|
+
},
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// ------------------------------------------------------- idea-inspiration/market
|
|
1581
|
+
/** 雷达→灵感:基于市场信号/生产底座/创意简报生成开书灵感。 */
|
|
1582
|
+
const marketIdeaInspirationRoute: WebRoute = {
|
|
1583
|
+
kind: 'exact',
|
|
1584
|
+
path: NOVEL_API.ideaInspirationMarket,
|
|
1585
|
+
handler: async (req, res) => {
|
|
1586
|
+
if (!guard(req, res, 'POST')) return
|
|
1587
|
+
const config = getConfig()
|
|
1588
|
+
const body = await readJsonBody<{ signals?: import('./protocol.ts').MarketRadarSignal[]; foundation?: import('./protocol.ts').ProductionFoundation; brief?: import('./protocol.ts').MarketCreativeBrief; count?: number }>(req)
|
|
1589
|
+
if (body === undefined) {
|
|
1590
|
+
writeJson(res, 400, { error: '缺少市场分析参数' })
|
|
1591
|
+
return
|
|
1592
|
+
}
|
|
1593
|
+
try {
|
|
1594
|
+
const result = await runMarketIdeaInspiration(ctx, config, body)
|
|
1595
|
+
writeJson(res, 200, { result })
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
writeJson(res, 500, { error: (error as Error).message })
|
|
1598
|
+
}
|
|
1599
|
+
},
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// ------------------------------------------------------------- director
|
|
1603
|
+
/** 自动导演编排建议:基于全书上下文。 */
|
|
1604
|
+
const directorRoute: WebRoute = {
|
|
1605
|
+
kind: 'exact',
|
|
1606
|
+
path: NOVEL_API.director,
|
|
1607
|
+
handler: async (req, res) => {
|
|
1608
|
+
if (!guard(req, res, 'POST')) return
|
|
1609
|
+
const config = getConfig()
|
|
1610
|
+
const body = await readJsonBody<DirectorRequest & { bookId?: string }>(req)
|
|
1611
|
+
const outputDir = resolveOutputDir(config, body?.bookId)
|
|
1612
|
+
const project = loadProject(outputDir)
|
|
1613
|
+
if (project === undefined) {
|
|
1614
|
+
writeJson(res, 400, { error: '输出目录中没有项目' })
|
|
1615
|
+
return
|
|
1616
|
+
}
|
|
1617
|
+
try {
|
|
1618
|
+
const result = await runDirectorAdvice(ctx, config, project, body ?? {})
|
|
1619
|
+
writeJson(res, 200, { result })
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
writeJson(res, 500, { error: (error as Error).message })
|
|
1622
|
+
}
|
|
1623
|
+
},
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// ------------------------------------------------------------- director/todos
|
|
1627
|
+
/** 自动导演「采纳」出的书内待办:GET 读取 / POST 增删勾选(按书路由)。 */
|
|
1628
|
+
const directorTodosRoute: WebRoute = {
|
|
1629
|
+
kind: 'exact',
|
|
1630
|
+
path: NOVEL_API.directorTodos,
|
|
1631
|
+
handler: async (req, res) => {
|
|
1632
|
+
type DirectorTodosReq = { op?: 'add' | 'toggle' | 'remove'; text?: string; source?: DirectorTodo['source']; id?: string; bookId?: string }
|
|
1633
|
+
let body: DirectorTodosReq | undefined
|
|
1634
|
+
if (req.method === 'POST') {
|
|
1635
|
+
if (!guard(req, res, 'POST')) return
|
|
1636
|
+
body = await readJsonBody<DirectorTodosReq>(req)
|
|
1637
|
+
} else {
|
|
1638
|
+
if (!guard(req, res, 'GET')) return
|
|
1639
|
+
}
|
|
1640
|
+
const config = getConfig()
|
|
1641
|
+
const outputDir = resolveOutputDir(config, body?.bookId)
|
|
1642
|
+
const project = loadProject(outputDir)
|
|
1643
|
+
if (project === undefined) {
|
|
1644
|
+
writeJson(res, 400, { error: '输出目录中没有项目' })
|
|
1645
|
+
return
|
|
1646
|
+
}
|
|
1647
|
+
project.todos ??= []
|
|
1648
|
+
if (req.method === 'POST' && body !== undefined) {
|
|
1649
|
+
if (body.op === 'add' && (body.text ?? '').trim() !== '') {
|
|
1650
|
+
project.todos.unshift({ id: `td-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`, text: body.text!.trim(), source: body.source ?? 'fix', done: false, createdAt: new Date().toISOString() })
|
|
1651
|
+
} else if (body.op === 'toggle' && body.id !== undefined) {
|
|
1652
|
+
project.todos = project.todos.map(t => t.id === body.id ? { ...t, done: !t.done } : t)
|
|
1653
|
+
} else if (body.op === 'remove' && body.id !== undefined) {
|
|
1654
|
+
project.todos = project.todos.filter(t => t.id !== body.id)
|
|
1655
|
+
}
|
|
1656
|
+
project.updatedAt = new Date().toISOString()
|
|
1657
|
+
saveProject(outputDir, project)
|
|
1658
|
+
}
|
|
1659
|
+
writeJson(res, 200, { todos: project.todos })
|
|
1660
|
+
},
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// ------------------------------------------------------------- llm-live
|
|
1664
|
+
/** AI 创作实况:订阅 LLM 实时事件流(NDJSON;订阅时先重放最近缓冲)。 */
|
|
1665
|
+
const llmLiveRoute: WebRoute = {
|
|
1666
|
+
kind: 'exact',
|
|
1667
|
+
path: NOVEL_API.llmLive,
|
|
1668
|
+
handler: async (req, res) => {
|
|
1669
|
+
if (req.method !== 'GET') {
|
|
1670
|
+
writeJson(res, 405, { error: 'GET only' })
|
|
1671
|
+
return
|
|
1672
|
+
}
|
|
1673
|
+
if (!isLoopbackRequest(req)) {
|
|
1674
|
+
writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
1675
|
+
return
|
|
1676
|
+
}
|
|
1677
|
+
res.writeHead(200, {
|
|
1678
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
1679
|
+
'cache-control': 'no-cache',
|
|
1680
|
+
'x-accel-buffering': 'no',
|
|
1681
|
+
'referrer-policy': 'no-referrer',
|
|
1682
|
+
})
|
|
1683
|
+
const unsubscribe = subscribeLiveFeed((frame) => {
|
|
1684
|
+
try { res.write('data: ' + JSON.stringify(frame) + '\n\n') } catch { /* client closed */ }
|
|
1685
|
+
})
|
|
1686
|
+
const hb = setInterval(() => { try { res.write(': ping\n\n') } catch { /* closed */ } }, 25000)
|
|
1687
|
+
// 关键:让 handler 一直挂起,直到客户端关闭,避免 WebRoute 框架在 handler 返回后关闭响应。
|
|
1688
|
+
await new Promise<void>((resolve) => {
|
|
1689
|
+
res.on('close', () => {
|
|
1690
|
+
clearInterval(hb)
|
|
1691
|
+
unsubscribe()
|
|
1692
|
+
resolve()
|
|
1693
|
+
})
|
|
1694
|
+
})
|
|
1695
|
+
},
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1200
1698
|
// ------------------------------------------------------------- bookshelf
|
|
1201
1699
|
const bookshelfRoute: WebRoute = {
|
|
1202
1700
|
kind: 'exact',
|
|
@@ -1408,9 +1906,15 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
1408
1906
|
path: NOVEL_API.audit,
|
|
1409
1907
|
handler: async (req, res) => {
|
|
1410
1908
|
if (!guard(req, res, 'POST')) return
|
|
1411
|
-
const
|
|
1412
|
-
const
|
|
1413
|
-
|
|
1909
|
+
const body = await readJsonBody<{ bookId?: string }>(req)
|
|
1910
|
+
const config0 = getConfig()
|
|
1911
|
+
const outputDir = resolveOutputDir(config0, body?.bookId)
|
|
1912
|
+
const config = { ...config0, outputDir }
|
|
1913
|
+
const project = loadProject(outputDir)
|
|
1914
|
+
if (project === undefined) {
|
|
1915
|
+
writeJson(res, 400, { error: '输出目录中没有项目' })
|
|
1916
|
+
return
|
|
1917
|
+
}
|
|
1414
1918
|
const startedAt = new Date().toISOString()
|
|
1415
1919
|
auditState = { status: 'running', startedAt, totalBatches: 0, completedBatches: 0 }
|
|
1416
1920
|
try {
|
|
@@ -1433,6 +1937,7 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
1433
1937
|
issues,
|
|
1434
1938
|
auditedChapters,
|
|
1435
1939
|
auditedAt: auditState.finishedAt!,
|
|
1940
|
+
model: config.auditModel || config.model,
|
|
1436
1941
|
}
|
|
1437
1942
|
writeJson(res, 200, response)
|
|
1438
1943
|
} catch (error) {
|
|
@@ -2930,9 +3435,10 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
2930
3435
|
path: NOVEL_API.runStart,
|
|
2931
3436
|
handler: async (req, res) => {
|
|
2932
3437
|
if (!guard(req, res, 'POST')) return
|
|
2933
|
-
const body = await readJsonBody<RunStartRequest>(req)
|
|
3438
|
+
const body = await readJsonBody<RunStartRequest & { bookId?: string }>(req)
|
|
2934
3439
|
const config = getConfig()
|
|
2935
|
-
const
|
|
3440
|
+
const outputDir = resolveOutputDir(config, body?.bookId)
|
|
3441
|
+
const project = loadProject(outputDir)
|
|
2936
3442
|
if (project === undefined) {
|
|
2937
3443
|
writeJson(res, 400, { error: '输出目录中没有项目' })
|
|
2938
3444
|
return
|
|
@@ -2949,7 +3455,7 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
2949
3455
|
return
|
|
2950
3456
|
}
|
|
2951
3457
|
try {
|
|
2952
|
-
const state = await runner.start(startNo, endNo)
|
|
3458
|
+
const state = await runner.start(startNo, endNo, outputDir)
|
|
2953
3459
|
writeJson(res, 200, state satisfies RunState)
|
|
2954
3460
|
} catch (error) {
|
|
2955
3461
|
writeJson(res, 409, { error: (error as Error).message })
|
|
@@ -3340,6 +3846,20 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
3340
3846
|
chapterSaveRoute,
|
|
3341
3847
|
assetsRoute,
|
|
3342
3848
|
styleEngineRoute,
|
|
3849
|
+
styleFormulaRoute,
|
|
3850
|
+
styleDetectRoute,
|
|
3851
|
+
marketRadarRoute,
|
|
3852
|
+
marketRadarScanRoute,
|
|
3853
|
+
marketRadarApplyRoute,
|
|
3854
|
+
marketRadarSyncRoute,
|
|
3855
|
+
marketRadarBriefRoute,
|
|
3856
|
+
knowledgeRoute,
|
|
3857
|
+
bookAnalysisRoute,
|
|
3858
|
+
ideaInspirationRoute,
|
|
3859
|
+
marketIdeaInspirationRoute,
|
|
3860
|
+
directorRoute,
|
|
3861
|
+
directorTodosRoute,
|
|
3862
|
+
llmLiveRoute,
|
|
3343
3863
|
assistantRoute,
|
|
3344
3864
|
assistantHistoryRoute,
|
|
3345
3865
|
assistantClearRoute,
|