@waterwx/dsh-novel-forge 1.4.0 → 1.4.1

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
@@ -39,6 +39,8 @@ import {
39
39
  type BookImportDirResponse,
40
40
  type BookImportTextRequest,
41
41
  type BookImportTextResponse,
42
+ type BookImportTextPreviewRequest,
43
+ type BookImportTextPreviewResponse,
42
44
  type BookRemoveRequest,
43
45
  type BookshelfSnapshot,
44
46
  type ChapterResponse,
@@ -101,6 +103,8 @@ import {
101
103
  createProject,
102
104
  exportBook,
103
105
  importBookText,
106
+ importBookTextFromText,
107
+ previewBookText,
104
108
  extractBible,
105
109
  extractStyleAsset,
106
110
  extractWorld,
@@ -128,6 +132,7 @@ import {
128
132
  extractVisualRules,
129
133
  extractRoleVisual,
130
134
  suggestOutlines,
135
+ reverseOutlineFromChapters,
131
136
  breakdownBook,
132
137
  generateRoleReferenceImage,
133
138
  checkSensitiveText,
@@ -139,7 +144,7 @@ import {
139
144
  } from './engine.ts'
140
145
 
141
146
  /** Cap on JSON request bodies (generous: cover images travel as base64). */
142
- const MAX_JSON_BODY_BYTES = 16 * 1024 * 1024
147
+ const MAX_JSON_BODY_BYTES = 64 * 1024 * 1024
143
148
 
144
149
  /** Loopback-only fence (mirrors the family plugins' pairing routes). */
145
150
  function isLoopbackRequest(request: IncomingMessage): boolean {
@@ -1236,23 +1241,35 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
1236
1241
  }
1237
1242
 
1238
1243
  // ------------------------------------------- bookshelf import-text
1239
- /** 导入 txt/md 全本(Mode B):拆章落盘建项目,登记书架。 */
1244
+ /** 导入 txt/md 全本(Mode B):拆章落盘建项目,登记书架。支持浏览器上传(text+fileName)与服务器本地文件(filePath)两种模式。 */
1240
1245
  const bookshelfImportTextRoute: WebRoute = {
1241
1246
  kind: 'exact',
1242
1247
  path: NOVEL_API.bookshelfImportText,
1243
1248
  handler: async (req, res) => {
1244
1249
  if (!guard(req, res, 'POST')) return
1245
1250
  const body = await readJsonBody<BookImportTextRequest>(req)
1246
- const filePath = body?.filePath?.trim()
1247
- if (filePath === undefined || filePath === '') {
1248
- writeJson(res, 400, { error: 'filePath 不能为空' })
1249
- return
1250
- }
1251
- if (!existsSync(filePath)) {
1252
- writeJson(res, 400, { error: `文件不存在:${filePath}` })
1253
- return
1254
- }
1255
1251
  try {
1252
+ // 模式二:浏览器上传全文内容
1253
+ if (body?.text !== undefined && body.text.length > 0) {
1254
+ const bookName = (body.fileName ?? '').replace(/\.[^.]+$/, '').trim().slice(0, 40) || '导入小说'
1255
+ const outDir = body?.outputDir?.trim() !== undefined && body.outputDir.trim() !== ''
1256
+ ? body.outputDir.trim()
1257
+ : defaultOutputDirFor(bookName)
1258
+ const result = importBookTextFromText(body.text, outDir, bookName)
1259
+ const { book } = importDir(outDir)
1260
+ writeJson(res, 200, { ...result, book })
1261
+ return
1262
+ }
1263
+ // 模式一:服务器本地文件
1264
+ const filePath = body?.filePath?.trim()
1265
+ if (filePath === undefined || filePath === '') {
1266
+ writeJson(res, 400, { error: 'filePath 不能为空(或请上传文件内容)' })
1267
+ return
1268
+ }
1269
+ if (!existsSync(filePath)) {
1270
+ writeJson(res, 400, { error: `文件不存在:${filePath}` })
1271
+ return
1272
+ }
1256
1273
  const bookName = basename(filePath, extname(filePath)).slice(0, 40) || '导入小说'
1257
1274
  const outDir = body?.outputDir?.trim() !== undefined && body.outputDir.trim() !== ''
1258
1275
  ? body.outputDir.trim()
@@ -1266,6 +1283,28 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
1266
1283
  },
1267
1284
  }
1268
1285
 
1286
+ // --------------------------------------- bookshelf import-text preview
1287
+ /** 上传全文做拆章预览(不落盘、不登记书架)。 */
1288
+ const bookshelfImportTextPreviewRoute: WebRoute = {
1289
+ kind: 'exact',
1290
+ path: NOVEL_API.bookshelfImportTextPreview,
1291
+ handler: async (req, res) => {
1292
+ if (!guard(req, res, 'POST')) return
1293
+ const body = await readJsonBody<BookImportTextPreviewRequest>(req)
1294
+ if (body?.text === undefined || body.text.length === 0) {
1295
+ writeJson(res, 400, { error: 'text 不能为空' })
1296
+ return
1297
+ }
1298
+ try {
1299
+ const bookName = (body.fileName ?? '').replace(/\.[^.]+$/, '').trim().slice(0, 40) || '导入小说'
1300
+ const preview = previewBookText(body.text)
1301
+ writeJson(res, 200, { bookName, ...preview })
1302
+ } catch (err) {
1303
+ writeJson(res, 400, { error: err instanceof Error ? err.message : String(err) })
1304
+ }
1305
+ },
1306
+ }
1307
+
1269
1308
  // ---------------------------------------------------------------- audit
1270
1309
  /** 全书一致性质检。 */
1271
1310
  const auditRoute: WebRoute = {
@@ -2071,6 +2110,42 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
2071
2110
  },
2072
2111
  }
2073
2112
 
2113
+ // ------------------------------------------------- outline reverse
2114
+ /** 反推大纲:从已写章节正文反向生成全书总纲(NDJSON 流式进度)。 */
2115
+ const outlineReverseRoute: WebRoute = {
2116
+ kind: 'exact',
2117
+ path: NOVEL_API.outlineReverse,
2118
+ handler: async (req, res) => {
2119
+ if (!guard(req, res, 'POST')) return
2120
+ const config = getConfig()
2121
+ const project = requireProject(res)
2122
+ if (project === undefined) return
2123
+ res.writeHead(200, {
2124
+ 'content-type': 'application/x-ndjson; charset=utf-8',
2125
+ 'cache-control': 'no-cache',
2126
+ 'x-accel-buffering': 'no',
2127
+ 'referrer-policy': 'no-referrer',
2128
+ })
2129
+ const send = (frame: JobFrame): void => {
2130
+ res.write(JSON.stringify(frame) + '\n')
2131
+ }
2132
+ try {
2133
+ const outline = await reverseOutlineFromChapters(ctx, config, project, config.outputDir, (done, total, phase) => {
2134
+ send({ type: 'outline-progress', done, total, phase })
2135
+ })
2136
+ // 保存总纲(仅更新文本,不动书名/进度)。
2137
+ project.outline = outline
2138
+ project.updatedAt = new Date().toISOString()
2139
+ saveProject(config.outputDir, project)
2140
+ send({ type: 'outline-done', outline, chars: outline.length })
2141
+ } catch (error) {
2142
+ send({ type: 'error', no: 0, message: (error as Error).message })
2143
+ } finally {
2144
+ res.end()
2145
+ }
2146
+ },
2147
+ }
2148
+
2074
2149
  // ----------------------------------------------------------- breakdown
2075
2150
  /** 拆书分析:对已写章节做结构/人物/文风/卖点体检(两阶段:源笔记→分节分析)。 */
2076
2151
  const breakdownRoute: WebRoute = {
@@ -2291,6 +2366,7 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
2291
2366
  bookshelfRemoveRoute,
2292
2367
  bookshelfImportDirRoute,
2293
2368
  bookshelfImportTextRoute,
2369
+ bookshelfImportTextPreviewRoute,
2294
2370
  resetRoute,
2295
2371
  auditRoute,
2296
2372
  charactersRefreshRoute,
@@ -2311,6 +2387,7 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
2311
2387
  configRoute,
2312
2388
  openFolderRoute,
2313
2389
  outlineSuggestRoute,
2390
+ outlineReverseRoute,
2314
2391
  breakdownRoute,
2315
2392
  runStartRoute,
2316
2393
  runControlRoute,