@waterwx/dsh-novel-forge 1.9.0 → 1.10.0
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 +3 -0
- package/lib/client.js +1978 -599
- package/lib/client.js.map +1 -1
- package/lib/index.js +679 -34
- package/lib/index.js.map +1 -1
- package/lib/types/assets.d.ts +3 -1
- package/lib/types/client/api.d.ts +11 -1
- package/lib/types/client/panel/AdaptModeView.d.ts +2 -1
- package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +4 -0
- package/lib/types/engine.d.ts +38 -1
- package/lib/types/protocol.d.ts +165 -4
- package/package.json +1 -1
- package/src/assets.ts +16 -1
- package/src/author-assets.ts +7 -26
- package/src/client/api.ts +61 -0
- package/src/client/panel/AdaptModeView.tsx +377 -59
- package/src/client/panel/AuthorAssetsView.tsx +3 -1
- package/src/client/panel/AuthorHome.tsx +118 -23
- package/src/client/panel/GlobalAssetLibraryView.tsx +172 -0
- package/src/client/panel/panel.module.css +25 -0
- package/src/engine.ts +234 -2
- package/src/protocol.ts +161 -4
- package/src/routes.ts +206 -3
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
1
|
+
import { homedir, tmpdir } from "node:os";
|
|
2
2
|
import { basename, extname, join } from "node:path";
|
|
3
3
|
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
4
4
|
import z from "schemastery";
|
|
@@ -529,6 +529,157 @@ const BUILTIN_PROGRESSION_MODES = [
|
|
|
529
529
|
primary: false
|
|
530
530
|
}
|
|
531
531
|
];
|
|
532
|
+
/** 剧情桥段库:可复用情节套路(作者阅读经验沉淀,非某本书的剧情线)。 */
|
|
533
|
+
const BUILTIN_PLOT_BEATS = [
|
|
534
|
+
{
|
|
535
|
+
key: "play-weak",
|
|
536
|
+
name: "扮猪吃虎",
|
|
537
|
+
category: "装逼打脸",
|
|
538
|
+
summary: "主角示弱/隐藏实力,关键时刻亮出獠牙,反差打脸。",
|
|
539
|
+
position: "前期/中期",
|
|
540
|
+
preconditions: ["主角有远超表面的实力", "有旁观者低估他"],
|
|
541
|
+
payoffSource: [
|
|
542
|
+
"身份反差",
|
|
543
|
+
"实力揭晓时众人错愕",
|
|
544
|
+
"质疑者被打脸"
|
|
545
|
+
],
|
|
546
|
+
combos: ["打脸", "身份揭露"],
|
|
547
|
+
taboos: ["示弱太久读者憋屈", "反转无铺垫"],
|
|
548
|
+
applicableGenres: [
|
|
549
|
+
"都市",
|
|
550
|
+
"玄幻",
|
|
551
|
+
"仙侠"
|
|
552
|
+
]
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
key: "face-slap",
|
|
556
|
+
name: "打脸",
|
|
557
|
+
category: "装逼打脸",
|
|
558
|
+
summary: "挑衅者趾高气扬,主角一击让其灰头土脸,情绪宣泄。",
|
|
559
|
+
position: "任何",
|
|
560
|
+
preconditions: ["挑衅者有明确的优越感", "主角有强出一截的底牌"],
|
|
561
|
+
payoffSource: ["当众羞辱反杀", "围观者态度反转"],
|
|
562
|
+
combos: ["扮猪吃虎", "大比"],
|
|
563
|
+
taboos: ["连续无新意打脸", "羞辱过头读者不适"],
|
|
564
|
+
applicableGenres: [
|
|
565
|
+
"都市",
|
|
566
|
+
"玄幻",
|
|
567
|
+
"仙侠"
|
|
568
|
+
]
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
key: "broken-engagement",
|
|
572
|
+
name: "退婚/反悔",
|
|
573
|
+
category: "身份逆袭",
|
|
574
|
+
summary: "被退婚/被轻视→用实力打回,顺势立目标。",
|
|
575
|
+
position: "开局/前期",
|
|
576
|
+
preconditions: ["主角处于弱势", "退婚方势利"],
|
|
577
|
+
payoffSource: ["被轻贱→反杀", "立下目标"],
|
|
578
|
+
combos: ["打脸", "金手指亮相"],
|
|
579
|
+
taboos: ["拖大几十章才洗", "女主误会太久"],
|
|
580
|
+
applicableGenres: [
|
|
581
|
+
"玄幻",
|
|
582
|
+
"都市",
|
|
583
|
+
"豪门"
|
|
584
|
+
]
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
key: "auction-bargain",
|
|
588
|
+
name: "拍卖会捡漏",
|
|
589
|
+
category: "机缘",
|
|
590
|
+
summary: "主角在拍卖会上用独到眼光/信息差捞到被低估的宝物。",
|
|
591
|
+
position: "中期",
|
|
592
|
+
preconditions: ["主角有识货能力/金手指", "现场有争夺者"],
|
|
593
|
+
payoffSource: ["捡漏爽", "争夺者懊悔"],
|
|
594
|
+
combos: ["渔翁得利", "宝物升级"],
|
|
595
|
+
taboos: ["全程顺风顺水无波折", "宝物无后续用处"],
|
|
596
|
+
applicableGenres: [
|
|
597
|
+
"玄幻",
|
|
598
|
+
"仙侠",
|
|
599
|
+
"都市"
|
|
600
|
+
]
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
key: "secret-realm",
|
|
604
|
+
name: "秘境/夺宝",
|
|
605
|
+
category: "机缘",
|
|
606
|
+
summary: "秘境开启,主角入内夺宝/悟道,或遇强敌/阴谋。",
|
|
607
|
+
position: "中期/后期",
|
|
608
|
+
preconditions: ["秘境有明确利益", "竞争者众多"],
|
|
609
|
+
payoffSource: ["机缘得宝", "危机中成长"],
|
|
610
|
+
combos: ["夺宝", "绝境翻盘"],
|
|
611
|
+
taboos: ["宝物白给", "危机虎头蛇尾"],
|
|
612
|
+
applicableGenres: [
|
|
613
|
+
"仙侠",
|
|
614
|
+
"玄幻",
|
|
615
|
+
"悬疑"
|
|
616
|
+
]
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
key: "misunderstanding",
|
|
620
|
+
name: "误会/解释不清",
|
|
621
|
+
category: "情感拉扯",
|
|
622
|
+
summary: "一方误解另一方的行为,情绪拉满后解开。",
|
|
623
|
+
position: "任何",
|
|
624
|
+
preconditions: ["信息不对称", "两方都在意关系"],
|
|
625
|
+
payoffSource: ["误会造痛", "和解破冰"],
|
|
626
|
+
combos: ["关系拉扯", "身份揭露"],
|
|
627
|
+
taboos: ["误会拖太久", "为虐而虐"],
|
|
628
|
+
applicableGenres: [
|
|
629
|
+
"都市",
|
|
630
|
+
"情感",
|
|
631
|
+
"古言"
|
|
632
|
+
]
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
key: "last-stand",
|
|
636
|
+
name: "绝境翻盘",
|
|
637
|
+
category: "战斗",
|
|
638
|
+
summary: "主角被逼到绝境,借助底牌/意志完成反杀。",
|
|
639
|
+
position: "高潮",
|
|
640
|
+
preconditions: ["主角实力明显劣势", "有破局底牌"],
|
|
641
|
+
payoffSource: ["绝处逢生", "反派嚣张后被打脸"],
|
|
642
|
+
combos: ["金手指爆发", "打脸"],
|
|
643
|
+
taboos: ["开挂太突兀", "翻盘无代价"],
|
|
644
|
+
applicableGenres: [
|
|
645
|
+
"玄幻",
|
|
646
|
+
"武侠",
|
|
647
|
+
"无限流"
|
|
648
|
+
]
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
key: "tournament",
|
|
652
|
+
name: "大比/比试",
|
|
653
|
+
category: "升级打脸",
|
|
654
|
+
summary: "宗门/势力大比,主角一路过关,暴露实力、收获名声。",
|
|
655
|
+
position: "中期/后期",
|
|
656
|
+
preconditions: ["有正式舞台", "有看客与对手"],
|
|
657
|
+
payoffSource: ["连胜升级", "众目睽睽下打脸"],
|
|
658
|
+
combos: ["打脸", "身份揭露"],
|
|
659
|
+
taboos: ["比赛水太多", "对手全送脸"],
|
|
660
|
+
applicableGenres: [
|
|
661
|
+
"仙侠",
|
|
662
|
+
"玄幻",
|
|
663
|
+
"体育竞技"
|
|
664
|
+
]
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
key: "identity-reveal",
|
|
668
|
+
name: "身份揭露",
|
|
669
|
+
category: "反转",
|
|
670
|
+
summary: "主角/配角的隐藏身份在关键时点揭开,颠覆认知。",
|
|
671
|
+
position: "中期/后期",
|
|
672
|
+
preconditions: ["身份有可埋伏的线索", "揭开时机能引爆情绪"],
|
|
673
|
+
payoffSource: ["认知颠覆", "立场反转"],
|
|
674
|
+
combos: ["打脸", "误会解开"],
|
|
675
|
+
taboos: ["毫无伏笔硬揭", "身份设定无意义"],
|
|
676
|
+
applicableGenres: [
|
|
677
|
+
"悬疑",
|
|
678
|
+
"古言",
|
|
679
|
+
"玄幻"
|
|
680
|
+
]
|
|
681
|
+
}
|
|
682
|
+
];
|
|
532
683
|
/** 默认(空)项目写作资产。 */
|
|
533
684
|
function emptyProjectAssets() {
|
|
534
685
|
return {
|
|
@@ -1297,6 +1448,8 @@ const NOVEL_API = {
|
|
|
1297
1448
|
runStatus: "/api/dsh-novel-forge/run/status",
|
|
1298
1449
|
config: "/api/dsh-novel-forge/config",
|
|
1299
1450
|
openFolder: "/api/dsh-novel-forge/open-folder",
|
|
1451
|
+
/** 插件自更新:在 DSH profile 目录拉取最新 npm 版(下载后需重启 DSH 生效)。 */
|
|
1452
|
+
pluginUpdate: "/api/dsh-novel-forge/plugin/update",
|
|
1300
1453
|
/** 作者资产库/总数据:读取个人跨书资产(笔法/红线/套路/角色模板/世界观模板)。 */
|
|
1301
1454
|
authorAssets: "/api/dsh-novel-forge/author-assets",
|
|
1302
1455
|
/** 作者资产库:新增/更新一条资产(upsert by id)。 */
|
|
@@ -1311,6 +1464,14 @@ const NOVEL_API = {
|
|
|
1311
1464
|
adaptPropose: "/api/dsh-novel-forge/adapt/propose",
|
|
1312
1465
|
/** 改编模式:执行术语替换(全局替换 + 命中统计 + 改编文本预览)。 */
|
|
1313
1466
|
adaptExecute: "/api/dsh-novel-forge/adapt/execute",
|
|
1467
|
+
/** 改编模式:保存改编全文为新书(原书保留,登记书架)。 */
|
|
1468
|
+
adaptSave: "/api/dsh-novel-forge/adapt/save",
|
|
1469
|
+
/** 改编模式:从源全文 + 编辑后方案提炼新书资料并保存为「待写新书」。 */
|
|
1470
|
+
adaptMaterialize: "/api/dsh-novel-forge/adapt/materialize",
|
|
1471
|
+
/** 改编模式:rewrite 逐章重写(NDJSON 流式进度)。 */
|
|
1472
|
+
adaptRewriteStream: "/api/dsh-novel-forge/adapt/rewrite-stream",
|
|
1473
|
+
/** 改编模式:把预览/微调后的新书资料写入并登记书架。 */
|
|
1474
|
+
adaptMaterializeSave: "/api/dsh-novel-forge/adapt/materialize-save",
|
|
1314
1475
|
/** 主题自定义背景:上传图片(POST,存盘并返回服务端 URL)。 */
|
|
1315
1476
|
themeBackgroundUpload: "/api/dsh-novel-forge/theme/background",
|
|
1316
1477
|
/** 主题自定义背景:读取已上传文件(GET prefix,/theme/background/<name>)。 */
|
|
@@ -5042,6 +5203,262 @@ function normalizeAdaptationImpact(i) {
|
|
|
5042
5203
|
chapters: chapters !== void 0 && chapters.length > 0 ? chapters : void 0
|
|
5043
5204
|
};
|
|
5044
5205
|
}
|
|
5206
|
+
/** 改编模式 rewrite:逐章 LLM 重写(结构性改写,不只是换词)。
|
|
5207
|
+
* @returns 改写后的全文 + 逐章结果 + 保留原章的章号。 */
|
|
5208
|
+
async function rewriteAdaptationBook(ctx, config, text, mappings, rules, options = {}) {
|
|
5209
|
+
const chapters = splitBookText(text).filter((c) => c.body.length >= 50);
|
|
5210
|
+
if (chapters.length === 0) throw new Error("未能从全文拆出章节(内容过短或无章节结构)");
|
|
5211
|
+
const startNo = options.startNo ?? 1;
|
|
5212
|
+
const endNo = options.endNo ?? 0;
|
|
5213
|
+
const inWindow = (c) => c.no >= startNo && (endNo <= 0 || c.no <= endNo);
|
|
5214
|
+
const windowChapters = chapters.filter(inWindow);
|
|
5215
|
+
const cap = options.maxChapters !== void 0 && options.maxChapters > 0 ? Math.min(options.maxChapters, windowChapters.length) : windowChapters.length;
|
|
5216
|
+
const toRewrite = windowChapters.slice(0, cap);
|
|
5217
|
+
const toRewriteNos = new Set(toRewrite.map((c) => c.no));
|
|
5218
|
+
const total = toRewrite.length;
|
|
5219
|
+
let completed = 0;
|
|
5220
|
+
const mappingBlock = mappings.length > 0 ? "映射表(原值 → 新值):\n" + mappings.map((m) => `- ${m.source} → ${m.target}(${m.scope})${m.note !== void 0 && m.note !== "" ? ":" + m.note : ""}`).join("\n") : "";
|
|
5221
|
+
const ruleBlock = rules !== void 0 ? [
|
|
5222
|
+
rules.preserve.length > 0 ? "必须保留:\n" + rules.preserve.map((x) => `- ${x}`).join("\n") : "",
|
|
5223
|
+
rules.change.length > 0 ? "允许改变:\n" + rules.change.map((x) => `- ${x}`).join("\n") : "",
|
|
5224
|
+
rules.constraints.length > 0 ? "改编红线/一致性要求:\n" + rules.constraints.map((x) => `- ${x}`).join("\n") : ""
|
|
5225
|
+
].filter((s) => s !== "").join("\n") : "";
|
|
5226
|
+
const system = [
|
|
5227
|
+
"你是一位资深网文改编编剧。你会收到某一章的正文,以及本书的改编映射表与改编规则。",
|
|
5228
|
+
"任务:把这一章按改编方案**重写**成新版本(结构性改编,不只是换词)。",
|
|
5229
|
+
"要求:",
|
|
5230
|
+
"1. 严格遵守映射表(原值→新值),正文中所有源值都要替换为新值;涉及改名/改体系/改势力时,相关表述一起调整,使上下文自洽。",
|
|
5231
|
+
"2. 改编规则:必须保留的内容不得破坏(故事骨架/人物动机/伏笔逻辑/爽点结构);允许改变的内容可以放开调整;红线/一致性要求必须遵守。",
|
|
5232
|
+
"3. 若某个改动牵动叙事(如结局走向/时间线/世界观),要把这章的叙述顺势改得通顺、可信。",
|
|
5233
|
+
"4. 输出**只包含这一章重写后的正文**,不要重复标题,不要任何解释、开头或结尾。",
|
|
5234
|
+
"5. 字数与原章基本相当(允许 ±20%)。"
|
|
5235
|
+
].join("\n");
|
|
5236
|
+
const adaptedParts = [];
|
|
5237
|
+
const rewritten = [];
|
|
5238
|
+
const skipped = [];
|
|
5239
|
+
const hits = applyAdaptationReplacements(text, mappings).hits;
|
|
5240
|
+
for (const c of chapters) {
|
|
5241
|
+
const title = applyAdaptationMappings(c.title, mappings) || c.title;
|
|
5242
|
+
if (!toRewriteNos.has(c.no)) {
|
|
5243
|
+
adaptedParts.push("# 第" + c.no + "章 " + title + "\n\n" + c.body.trim() + "\n");
|
|
5244
|
+
continue;
|
|
5245
|
+
}
|
|
5246
|
+
const user = [
|
|
5247
|
+
mappingBlock,
|
|
5248
|
+
ruleBlock,
|
|
5249
|
+
"第 " + c.no + " 章《" + c.title + "》:",
|
|
5250
|
+
c.body
|
|
5251
|
+
].filter((s) => s !== "").join("\n\n");
|
|
5252
|
+
let body = "";
|
|
5253
|
+
try {
|
|
5254
|
+
body = stripRewriteHeading(await complete(ctx, config, {
|
|
5255
|
+
system,
|
|
5256
|
+
user,
|
|
5257
|
+
temperature: .7,
|
|
5258
|
+
maxTokens: Math.max(config.maxTokens, Math.min(16e3, c.body.length * 3)),
|
|
5259
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
5260
|
+
}));
|
|
5261
|
+
if (body.length < 50) body = "";
|
|
5262
|
+
} catch {
|
|
5263
|
+
body = "";
|
|
5264
|
+
}
|
|
5265
|
+
if (body === "") {
|
|
5266
|
+
skipped.push(c.no);
|
|
5267
|
+
body = c.body;
|
|
5268
|
+
}
|
|
5269
|
+
adaptedParts.push("# 第" + c.no + "章 " + title + "\n\n" + body.trim() + "\n");
|
|
5270
|
+
rewritten.push({
|
|
5271
|
+
no: c.no,
|
|
5272
|
+
title,
|
|
5273
|
+
chars: body.length
|
|
5274
|
+
});
|
|
5275
|
+
completed++;
|
|
5276
|
+
options.onProgress?.({
|
|
5277
|
+
completed,
|
|
5278
|
+
total,
|
|
5279
|
+
no: c.no,
|
|
5280
|
+
title
|
|
5281
|
+
});
|
|
5282
|
+
}
|
|
5283
|
+
return {
|
|
5284
|
+
adaptedText: adaptedParts.join("\n"),
|
|
5285
|
+
rewritten,
|
|
5286
|
+
skipped,
|
|
5287
|
+
hits
|
|
5288
|
+
};
|
|
5289
|
+
}
|
|
5290
|
+
/** 去掉 LLM 输出可能带上的 Markdown 标题行。 */
|
|
5291
|
+
function stripRewriteHeading(out) {
|
|
5292
|
+
return out.split(/\r?\n/).filter((line) => !/^\s*#/.test(line)).join("\n").trim();
|
|
5293
|
+
}
|
|
5294
|
+
/**
|
|
5295
|
+
* 改编模式 P3:从源全文 + 用户编辑后的改编方案,提炼新书资料并保存为「待写新书」。
|
|
5296
|
+
* 流程:源文导入临时项目 → 复用 extractBible/extractRoles/extractWorld 提炼 →
|
|
5297
|
+
* 按映射表把术语/人名/势力映射到新书命名层 → planVolumes/planChapters 生成待写计划 → 保存。
|
|
5298
|
+
* @returns 提炼后的新书资料(不含书架 book,由路由负责登记书架)。
|
|
5299
|
+
*/
|
|
5300
|
+
async function materializeAdaptedBook(ctx, config, args) {
|
|
5301
|
+
const bookName = (args.bookName ?? "").trim().slice(0, 40) || "改编新书";
|
|
5302
|
+
const outDir = (args.outputDir ?? "").trim();
|
|
5303
|
+
if (outDir === "") throw new Error("未指定新书输出目录");
|
|
5304
|
+
const mappings = args.proposal?.mappings ?? [];
|
|
5305
|
+
const tmpDir = join(tmpdir(), "dsh-novel-forge-adapt-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8));
|
|
5306
|
+
try {
|
|
5307
|
+
importBookTextFromText(args.text, tmpDir, bookName);
|
|
5308
|
+
const sourceProject = loadProject(tmpDir);
|
|
5309
|
+
if (sourceProject === void 0) throw new Error("临时项目创建失败");
|
|
5310
|
+
const sourceConfig = {
|
|
5311
|
+
...config,
|
|
5312
|
+
outputDir: tmpDir
|
|
5313
|
+
};
|
|
5314
|
+
const srcOutline = (args.outline ?? "").trim() !== "" ? (args.outline ?? "").trim() : fallbackSourceOutline(sourceProject);
|
|
5315
|
+
sourceProject.outline = srcOutline;
|
|
5316
|
+
let bible = await extractBible(ctx, sourceConfig, srcOutline, sourceProject);
|
|
5317
|
+
sourceProject.bible = bible;
|
|
5318
|
+
const roles = await extractRoles(ctx, sourceConfig, sourceProject);
|
|
5319
|
+
const world = await extractWorld(ctx, sourceConfig, sourceProject);
|
|
5320
|
+
const adaptedOutline = applyAdaptationMappings(srcOutline, mappings);
|
|
5321
|
+
bible = applyMappingsToBible(bible, mappings);
|
|
5322
|
+
const adaptedRoles = applyMappingsToRoles(roles, mappings);
|
|
5323
|
+
const adaptedWorld = applyMappingsToWorld(world, mappings);
|
|
5324
|
+
const project = createProject(bookName);
|
|
5325
|
+
project.bookName = bookName;
|
|
5326
|
+
project.outline = adaptedOutline;
|
|
5327
|
+
project.bible = bible;
|
|
5328
|
+
project.roles = adaptedRoles;
|
|
5329
|
+
project.world = adaptedWorld;
|
|
5330
|
+
project.volumes = await planVolumes(ctx, config, adaptedOutline);
|
|
5331
|
+
project.chapters = await planChapters(ctx, config, project, Math.max(1, Math.min(args.chapterCount ?? 30, 500)));
|
|
5332
|
+
return {
|
|
5333
|
+
bookName,
|
|
5334
|
+
outline: adaptedOutline,
|
|
5335
|
+
bible,
|
|
5336
|
+
roles: adaptedRoles,
|
|
5337
|
+
world: adaptedWorld,
|
|
5338
|
+
volumes: project.volumes ?? [],
|
|
5339
|
+
chapters: project.chapters,
|
|
5340
|
+
outputDir: outDir
|
|
5341
|
+
};
|
|
5342
|
+
} finally {
|
|
5343
|
+
try {
|
|
5344
|
+
rmSync(tmpDir, {
|
|
5345
|
+
recursive: true,
|
|
5346
|
+
force: true
|
|
5347
|
+
});
|
|
5348
|
+
} catch {}
|
|
5349
|
+
}
|
|
5350
|
+
}
|
|
5351
|
+
/** 把预览/微调后的新书资料写入输出目录并返回摘要(书架登记由路由负责)。 */
|
|
5352
|
+
function saveMaterializedBook(outDir, bookName, data) {
|
|
5353
|
+
const project = createProject(bookName);
|
|
5354
|
+
project.bookName = bookName;
|
|
5355
|
+
project.outline = data.outline;
|
|
5356
|
+
project.bible = data.bible;
|
|
5357
|
+
project.roles = data.roles;
|
|
5358
|
+
project.world = data.world;
|
|
5359
|
+
project.volumes = data.volumes;
|
|
5360
|
+
project.chapters = data.chapters;
|
|
5361
|
+
saveProject(outDir, project);
|
|
5362
|
+
return {
|
|
5363
|
+
bookName,
|
|
5364
|
+
chapters: data.chapters.length,
|
|
5365
|
+
outputDir: outDir
|
|
5366
|
+
};
|
|
5367
|
+
}
|
|
5368
|
+
/** 把一份文本按映射表做全局替换(复用术语替换执行器)。 */
|
|
5369
|
+
function applyAdaptationMappings(text, mappings) {
|
|
5370
|
+
return applyAdaptationReplacements(text, mappings).adaptedText;
|
|
5371
|
+
}
|
|
5372
|
+
/** 改编设定圣经:把人名/术语/势力按映射表替换。 */
|
|
5373
|
+
function applyMappingsToBible(bible, mappings) {
|
|
5374
|
+
const t = (s) => applyAdaptationMappings(s, mappings);
|
|
5375
|
+
return {
|
|
5376
|
+
...bible,
|
|
5377
|
+
genre: t(bible.genre),
|
|
5378
|
+
worldRules: bible.worldRules.map(t),
|
|
5379
|
+
redLines: bible.redLines.map(t),
|
|
5380
|
+
style: bible.style.map(t),
|
|
5381
|
+
characters: bible.characters.map((c) => ({
|
|
5382
|
+
...c,
|
|
5383
|
+
name: t(c.name),
|
|
5384
|
+
traits: c.traits.map(t),
|
|
5385
|
+
goals: t(c.goals),
|
|
5386
|
+
relations: t(c.relations),
|
|
5387
|
+
knowledge: c.knowledge?.map(t)
|
|
5388
|
+
}))
|
|
5389
|
+
};
|
|
5390
|
+
}
|
|
5391
|
+
/** 改编角色库:把人名/身份/标签/关系/成长/知情度按映射表替换。 */
|
|
5392
|
+
function applyMappingsToRoles(roles, mappings) {
|
|
5393
|
+
const t = (s) => applyAdaptationMappings(s, mappings);
|
|
5394
|
+
return roles.map((role) => ({
|
|
5395
|
+
...role,
|
|
5396
|
+
name: t(role.name),
|
|
5397
|
+
identity: t(role.identity),
|
|
5398
|
+
traits: role.traits.map(t),
|
|
5399
|
+
goals: t(role.goals),
|
|
5400
|
+
relations: role.relations.map(t),
|
|
5401
|
+
arc: role.arc.map(t),
|
|
5402
|
+
knowledge: role.knowledge.map(t),
|
|
5403
|
+
imagePrompt: role.imagePrompt !== void 0 ? {
|
|
5404
|
+
...role.imagePrompt,
|
|
5405
|
+
zh: t(role.imagePrompt.zh),
|
|
5406
|
+
en: t(role.imagePrompt.en),
|
|
5407
|
+
tags: role.imagePrompt.tags.map(t),
|
|
5408
|
+
source: role.imagePrompt.source !== void 0 ? t(role.imagePrompt.source) : void 0
|
|
5409
|
+
} : void 0,
|
|
5410
|
+
expressions: role.expressions?.map(t),
|
|
5411
|
+
promptKit: role.promptKit !== void 0 ? {
|
|
5412
|
+
...role.promptKit,
|
|
5413
|
+
portrait: {
|
|
5414
|
+
...role.promptKit.portrait,
|
|
5415
|
+
zh: t(role.promptKit.portrait.zh),
|
|
5416
|
+
en: t(role.promptKit.portrait.en)
|
|
5417
|
+
},
|
|
5418
|
+
sheet: {
|
|
5419
|
+
...role.promptKit.sheet,
|
|
5420
|
+
zh: t(role.promptKit.sheet.zh),
|
|
5421
|
+
en: t(role.promptKit.sheet.en)
|
|
5422
|
+
},
|
|
5423
|
+
expressions: role.promptKit.expressions.map((e) => ({
|
|
5424
|
+
...e,
|
|
5425
|
+
zh: t(e.zh),
|
|
5426
|
+
en: t(e.en)
|
|
5427
|
+
})),
|
|
5428
|
+
details: {
|
|
5429
|
+
...role.promptKit.details,
|
|
5430
|
+
zh: t(role.promptKit.details.zh),
|
|
5431
|
+
en: t(role.promptKit.details.en)
|
|
5432
|
+
}
|
|
5433
|
+
} : void 0
|
|
5434
|
+
}));
|
|
5435
|
+
}
|
|
5436
|
+
/** 改编大世界:境界/区域/势力名按映射表替换。 */
|
|
5437
|
+
function applyMappingsToWorld(world, mappings) {
|
|
5438
|
+
const t = (s) => applyAdaptationMappings(s, mappings);
|
|
5439
|
+
return {
|
|
5440
|
+
realms: world.realms.map((x) => ({
|
|
5441
|
+
name: t(x.name),
|
|
5442
|
+
description: t(x.description)
|
|
5443
|
+
})),
|
|
5444
|
+
regions: world.regions.map((x) => ({
|
|
5445
|
+
name: t(x.name),
|
|
5446
|
+
description: t(x.description),
|
|
5447
|
+
faction: x.faction !== void 0 ? t(x.faction) : void 0
|
|
5448
|
+
})),
|
|
5449
|
+
factions: world.factions.map((x) => ({
|
|
5450
|
+
name: t(x.name),
|
|
5451
|
+
kind: t(x.kind),
|
|
5452
|
+
description: t(x.description),
|
|
5453
|
+
region: x.region !== void 0 ? t(x.region) : void 0
|
|
5454
|
+
}))
|
|
5455
|
+
};
|
|
5456
|
+
}
|
|
5457
|
+
/** 反推大纲缺失时的兜底:用源书章节标题占位(可到工作区重新生成)。 */
|
|
5458
|
+
function fallbackSourceOutline(project) {
|
|
5459
|
+
const heads = project.chapters.map((c) => `第${c.no}章《${c.title}》:${c.beats !== void 0 && c.beats !== "" ? c.beats : ""}`).join("\n");
|
|
5460
|
+
return `# 《${project.bookName}》\n\n(反推大纲缺失,以下为章节标题占位,可在小说工坊重新生成大纲。)\n\n${heads}`;
|
|
5461
|
+
}
|
|
5045
5462
|
/**
|
|
5046
5463
|
* 摘要 + 事实抽取合并为一次 LLM 调用(省一次调用与一次正文输入,
|
|
5047
5464
|
* 批量生成时整体开销约省 25%)。
|
|
@@ -6963,23 +7380,14 @@ function importDefaultAuthorAssets() {
|
|
|
6963
7380
|
updatedAt: now
|
|
6964
7381
|
});
|
|
6965
7382
|
};
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
});
|
|
6975
|
-
for (const t of BUILTIN_STYLE_TEMPLATES) push("style", t.name, t.description, "分类:" + t.category + "\n适用:" + t.applicableGenres.join("、") + "\n叙述:" + t.proseRules.join(";") + "\n台词:" + t.dialogueRules.join(";") + "\n语言:" + t.languageRules.join(";") + "\n节奏:" + t.rhythmRules.join(";"), ["内置", "风格模板"], [], {
|
|
6976
|
-
category: t.category,
|
|
6977
|
-
applicableGenres: t.applicableGenres
|
|
6978
|
-
});
|
|
6979
|
-
for (const p of BUILTIN_PROGRESSION_MODES) push("progression", p.name, p.driver, "驱动:" + p.driver + "\n期待:" + p.readerExpectation + "\n兑现:" + p.payoffs.join(";") + "\n风险:" + p.risks.join(";"), ["内置", "推进"], [], {
|
|
6980
|
-
driver: p.driver,
|
|
6981
|
-
primary: p.primary
|
|
6982
|
-
});
|
|
7383
|
+
const builtinKinds = /* @__PURE__ */ new Set([
|
|
7384
|
+
"genre",
|
|
7385
|
+
"antiAi",
|
|
7386
|
+
"progression",
|
|
7387
|
+
"style",
|
|
7388
|
+
"plotBeat"
|
|
7389
|
+
]);
|
|
7390
|
+
library.items = library.items.filter((a) => !(a.tags.includes("内置") && builtinKinds.has(a.kind) && (a.sourceBooks ?? []).length === 0));
|
|
6983
7391
|
const shelf = loadBookshelf();
|
|
6984
7392
|
for (const book of shelf.books) {
|
|
6985
7393
|
const project = loadProject(book.outputDir);
|
|
@@ -7004,7 +7412,8 @@ function importDefaultAuthorAssets() {
|
|
|
7004
7412
|
for (const s of assets.styleAssets ?? []) push("style", s.name, s.proseRules.join(";"), "叙述:" + s.proseRules.join(";") + "\n台词:" + s.dialogueRules.join(";") + "\n描写:" + s.descriptionRules.join(";") + "\n边界:" + s.boundaries.join(";"), src, [], { sourceText: s.sourceText });
|
|
7005
7413
|
}
|
|
7006
7414
|
for (const role of project.roles ?? []) {
|
|
7007
|
-
|
|
7415
|
+
if (role.roleLabel !== "protagonist" && role.roleLabel !== "female_lead") continue;
|
|
7416
|
+
const persona = ("定位:" + role.roleLabel + "\n身份:" + role.identity + "\n性格:" + role.traits.join("、") + "\n目标:" + role.goals + "\n关系:" + role.relations.join("、") + "\n成长线:" + (role.arc.length > 0 ? role.arc.join(" > ") : "—") + "\n知情度:" + role.knowledge.join("、")).trim();
|
|
7008
7417
|
push("roleTemplate", role.name, role.identity, persona, src, [], {
|
|
7009
7418
|
roleLabel: role.roleLabel,
|
|
7010
7419
|
traits: role.traits,
|
|
@@ -7017,18 +7426,6 @@ function importDefaultAuthorAssets() {
|
|
|
7017
7426
|
saveAuthorAssets(library);
|
|
7018
7427
|
return library;
|
|
7019
7428
|
}
|
|
7020
|
-
/** 拍平题材库(含子题材,父级名称并入 tag)。 */
|
|
7021
|
-
function flattenGenres(nodes, parentName) {
|
|
7022
|
-
const out = [];
|
|
7023
|
-
for (const n of nodes) {
|
|
7024
|
-
out.push({
|
|
7025
|
-
...n,
|
|
7026
|
-
name: parentName !== void 0 ? parentName + "/" + n.name : n.name
|
|
7027
|
-
});
|
|
7028
|
-
if (n.children.length > 0) out.push(...flattenGenres(n.children, parentName !== void 0 ? parentName + "/" + n.name : n.name));
|
|
7029
|
-
}
|
|
7030
|
-
return out;
|
|
7031
|
-
}
|
|
7032
7429
|
//#endregion
|
|
7033
7430
|
//#region src/routes.ts
|
|
7034
7431
|
/** Cap on JSON request bodies (generous: cover images travel as base64). */
|
|
@@ -7903,7 +8300,8 @@ function makeRoutes(deps) {
|
|
|
7903
8300
|
genreLibrary: BUILTIN_GENRE_LIBRARY,
|
|
7904
8301
|
antiAiLibrary: BUILTIN_ANTI_AI_RULES,
|
|
7905
8302
|
styleTemplates: BUILTIN_STYLE_TEMPLATES,
|
|
7906
|
-
progressionLibrary: BUILTIN_PROGRESSION_MODES
|
|
8303
|
+
progressionLibrary: BUILTIN_PROGRESSION_MODES,
|
|
8304
|
+
plotBeatLibrary: BUILTIN_PLOT_BEATS
|
|
7907
8305
|
});
|
|
7908
8306
|
}
|
|
7909
8307
|
};
|
|
@@ -9104,6 +9502,42 @@ function makeRoutes(deps) {
|
|
|
9104
9502
|
});
|
|
9105
9503
|
}
|
|
9106
9504
|
};
|
|
9505
|
+
/** 插件自更新:在 DSH profile 目录拉取最新 npm 版(仅下载,需重启 DSH 生效)。 */
|
|
9506
|
+
const pluginUpdateRoute = {
|
|
9507
|
+
kind: "exact",
|
|
9508
|
+
path: NOVEL_API.pluginUpdate,
|
|
9509
|
+
handler: async (req, res) => {
|
|
9510
|
+
if (!guard(req, res, "POST")) return;
|
|
9511
|
+
const profileDir = join(homedir(), ".dsh", "profiles", "web");
|
|
9512
|
+
writeJson(res, 200, await new Promise((resolve) => {
|
|
9513
|
+
const child = spawn("pnpm", ["add", "@waterwx/dsh-novel-forge@latest"], {
|
|
9514
|
+
cwd: profileDir,
|
|
9515
|
+
shell: true
|
|
9516
|
+
});
|
|
9517
|
+
let acc = "";
|
|
9518
|
+
child.stdout.on("data", (d) => {
|
|
9519
|
+
acc += String(d);
|
|
9520
|
+
});
|
|
9521
|
+
child.stderr.on("data", (d) => {
|
|
9522
|
+
acc += String(d);
|
|
9523
|
+
});
|
|
9524
|
+
child.on("error", (error) => resolve({
|
|
9525
|
+
ok: false,
|
|
9526
|
+
message: "无法执行更新:" + error.message
|
|
9527
|
+
}));
|
|
9528
|
+
child.on("close", (code) => {
|
|
9529
|
+
if (code === 0) resolve({
|
|
9530
|
+
ok: true,
|
|
9531
|
+
message: "已更新到最新版本,请重启 DSH 生效。"
|
|
9532
|
+
});
|
|
9533
|
+
else resolve({
|
|
9534
|
+
ok: false,
|
|
9535
|
+
message: "更新失败(退出码 " + code + "):" + acc.slice(-400)
|
|
9536
|
+
});
|
|
9537
|
+
});
|
|
9538
|
+
}));
|
|
9539
|
+
}
|
|
9540
|
+
};
|
|
9107
9541
|
/** 开书想法 → AI 大纲:2-3 个可选方案(支持暂留换批:count 只补空槽 + exclude 避开已留方向)。 */
|
|
9108
9542
|
const outlineSuggestRoute = {
|
|
9109
9543
|
kind: "exact",
|
|
@@ -9725,17 +10159,223 @@ function makeRoutes(deps) {
|
|
|
9725
10159
|
return;
|
|
9726
10160
|
}
|
|
9727
10161
|
if (body?.mode === "rewrite") {
|
|
9728
|
-
|
|
10162
|
+
try {
|
|
10163
|
+
const result = await rewriteAdaptationBook(ctx, getConfig(), text, mappings, body?.rules, {
|
|
10164
|
+
maxChapters: body?.maxChapters,
|
|
10165
|
+
startNo: body?.startNo,
|
|
10166
|
+
endNo: body?.endNo
|
|
10167
|
+
});
|
|
10168
|
+
writeJson(res, 200, {
|
|
10169
|
+
adaptedText: result.adaptedText,
|
|
10170
|
+
mappings: mappings.length,
|
|
10171
|
+
hits: result.hits,
|
|
10172
|
+
mode: "rewrite",
|
|
10173
|
+
rewritten: result.rewritten,
|
|
10174
|
+
skipped: result.skipped
|
|
10175
|
+
});
|
|
10176
|
+
} catch (err) {
|
|
10177
|
+
writeJson(res, 400, { error: err instanceof Error ? err.message : String(err) });
|
|
10178
|
+
}
|
|
9729
10179
|
return;
|
|
9730
10180
|
}
|
|
9731
10181
|
const { adaptedText, hits } = applyAdaptationReplacements(text, mappings);
|
|
9732
10182
|
writeJson(res, 200, {
|
|
9733
10183
|
adaptedText,
|
|
9734
10184
|
mappings: mappings.length,
|
|
9735
|
-
hits
|
|
10185
|
+
hits,
|
|
10186
|
+
mode: "replace"
|
|
9736
10187
|
});
|
|
9737
10188
|
}
|
|
9738
10189
|
};
|
|
10190
|
+
/** rewrite 逐章重写:NDJSON 流式进度(支持分段 startNo/endNo)。 */
|
|
10191
|
+
const adaptRewriteStreamRoute = {
|
|
10192
|
+
kind: "exact",
|
|
10193
|
+
path: NOVEL_API.adaptRewriteStream,
|
|
10194
|
+
handler: async (req, res) => {
|
|
10195
|
+
if (!guard(req, res, "POST")) return;
|
|
10196
|
+
if (!getConfig().enableAdaptMode) {
|
|
10197
|
+
writeJson(res, 404, { error: "改编模式未开启" });
|
|
10198
|
+
return;
|
|
10199
|
+
}
|
|
10200
|
+
const body = await readJsonBody(req);
|
|
10201
|
+
const text = body?.text ?? "";
|
|
10202
|
+
const mappings = body?.mappings ?? [];
|
|
10203
|
+
if (text.length < 200 || mappings.length === 0) {
|
|
10204
|
+
writeJson(res, 400, { error: "请提供全文与映射表" });
|
|
10205
|
+
return;
|
|
10206
|
+
}
|
|
10207
|
+
res.writeHead(200, {
|
|
10208
|
+
"content-type": "application/x-ndjson; charset=utf-8",
|
|
10209
|
+
"cache-control": "no-cache",
|
|
10210
|
+
"x-accel-buffering": "no",
|
|
10211
|
+
"referrer-policy": "no-referrer"
|
|
10212
|
+
});
|
|
10213
|
+
const send = (frame) => {
|
|
10214
|
+
res.write(JSON.stringify(frame) + "\n");
|
|
10215
|
+
};
|
|
10216
|
+
try {
|
|
10217
|
+
const result = await rewriteAdaptationBook(ctx, getConfig(), text, mappings, body?.rules, {
|
|
10218
|
+
maxChapters: body?.maxChapters,
|
|
10219
|
+
startNo: body?.startNo,
|
|
10220
|
+
endNo: body?.endNo,
|
|
10221
|
+
onProgress: (info) => send({
|
|
10222
|
+
type: "progress",
|
|
10223
|
+
completed: info.completed,
|
|
10224
|
+
total: info.total,
|
|
10225
|
+
no: info.no,
|
|
10226
|
+
title: info.title
|
|
10227
|
+
})
|
|
10228
|
+
});
|
|
10229
|
+
send({
|
|
10230
|
+
type: "done",
|
|
10231
|
+
result: {
|
|
10232
|
+
adaptedText: result.adaptedText,
|
|
10233
|
+
mappings: mappings.length,
|
|
10234
|
+
hits: result.hits,
|
|
10235
|
+
mode: "rewrite",
|
|
10236
|
+
rewritten: result.rewritten,
|
|
10237
|
+
skipped: result.skipped
|
|
10238
|
+
}
|
|
10239
|
+
});
|
|
10240
|
+
} catch (err) {
|
|
10241
|
+
send({
|
|
10242
|
+
type: "error",
|
|
10243
|
+
message: err instanceof Error ? err.message : String(err)
|
|
10244
|
+
});
|
|
10245
|
+
} finally {
|
|
10246
|
+
res.end();
|
|
10247
|
+
}
|
|
10248
|
+
}
|
|
10249
|
+
};
|
|
10250
|
+
/** 保存改编全文为新书(原书保留,登记书架)。 */
|
|
10251
|
+
const adaptSaveRoute = {
|
|
10252
|
+
kind: "exact",
|
|
10253
|
+
path: NOVEL_API.adaptSave,
|
|
10254
|
+
handler: async (req, res) => {
|
|
10255
|
+
if (!guard(req, res, "POST")) return;
|
|
10256
|
+
if (!getConfig().enableAdaptMode) {
|
|
10257
|
+
writeJson(res, 404, { error: "改编模式未开启" });
|
|
10258
|
+
return;
|
|
10259
|
+
}
|
|
10260
|
+
const body = await readJsonBody(req);
|
|
10261
|
+
const text = (body?.text ?? "").trim();
|
|
10262
|
+
if (text.length < 200) {
|
|
10263
|
+
writeJson(res, 400, { error: "改编全文内容过短(<200 字符)" });
|
|
10264
|
+
return;
|
|
10265
|
+
}
|
|
10266
|
+
const bookName = (body?.bookName ?? "").trim().slice(0, 40) || "改编新书";
|
|
10267
|
+
const outDir = (body?.outputDir ?? "").trim() !== "" && body?.outputDir !== void 0 && body.outputDir.trim() !== "" ? body.outputDir.trim() : defaultOutputDirFor(bookName);
|
|
10268
|
+
const occupied = loadBookshelf().books.find((b) => b.outputDir === outDir);
|
|
10269
|
+
if (occupied !== void 0 && occupied.bookName !== bookName) {
|
|
10270
|
+
writeJson(res, 409, { error: `目录已被《${occupied.bookName}》占用,请更换书名或输出目录` });
|
|
10271
|
+
return;
|
|
10272
|
+
}
|
|
10273
|
+
try {
|
|
10274
|
+
const result = importBookTextFromText(text, outDir, bookName);
|
|
10275
|
+
const outline = (body?.outline ?? "").trim();
|
|
10276
|
+
if (outline !== "") {
|
|
10277
|
+
const project = loadProject(outDir);
|
|
10278
|
+
if (project !== void 0) {
|
|
10279
|
+
project.outline = outline;
|
|
10280
|
+
saveProject(outDir, project);
|
|
10281
|
+
}
|
|
10282
|
+
}
|
|
10283
|
+
const { book } = importDir(outDir);
|
|
10284
|
+
writeJson(res, 200, {
|
|
10285
|
+
book,
|
|
10286
|
+
bookName: result.bookName,
|
|
10287
|
+
chapters: result.chapters,
|
|
10288
|
+
skipped: result.skipped,
|
|
10289
|
+
outputDir: outDir
|
|
10290
|
+
});
|
|
10291
|
+
} catch (err) {
|
|
10292
|
+
writeJson(res, 400, { error: err instanceof Error ? err.message : String(err) });
|
|
10293
|
+
}
|
|
10294
|
+
}
|
|
10295
|
+
};
|
|
10296
|
+
/** 从源全文 + 编辑后方案提炼新书资料并保存为「待写新书」。 */
|
|
10297
|
+
const adaptMaterializeRoute = {
|
|
10298
|
+
kind: "exact",
|
|
10299
|
+
path: NOVEL_API.adaptMaterialize,
|
|
10300
|
+
handler: async (req, res) => {
|
|
10301
|
+
if (!guard(req, res, "POST")) return;
|
|
10302
|
+
if (!getConfig().enableAdaptMode) {
|
|
10303
|
+
writeJson(res, 404, { error: "改编模式未开启" });
|
|
10304
|
+
return;
|
|
10305
|
+
}
|
|
10306
|
+
const body = await readJsonBody(req);
|
|
10307
|
+
const text = (body?.text ?? "").trim();
|
|
10308
|
+
if (text.length < 200) {
|
|
10309
|
+
writeJson(res, 400, { error: "源书全文内容过短(<200 字符)" });
|
|
10310
|
+
return;
|
|
10311
|
+
}
|
|
10312
|
+
const proposal = body?.proposal;
|
|
10313
|
+
if (proposal === void 0 || proposal.mappings.length === 0) {
|
|
10314
|
+
writeJson(res, 400, { error: "请先提供至少一条映射的改编方案" });
|
|
10315
|
+
return;
|
|
10316
|
+
}
|
|
10317
|
+
const bookName = (body?.bookName ?? "").trim().slice(0, 40) || "改编新书";
|
|
10318
|
+
const outDir = (body?.outputDir ?? "").trim() !== "" && body?.outputDir !== void 0 && body.outputDir.trim() !== "" ? body.outputDir.trim() : defaultOutputDirFor(bookName);
|
|
10319
|
+
const occupied = loadBookshelf().books.find((b) => b.outputDir === outDir);
|
|
10320
|
+
if (occupied !== void 0 && occupied.bookName !== bookName) {
|
|
10321
|
+
writeJson(res, 409, { error: `目录已被《${occupied.bookName}》占用,请更换书名或输出目录` });
|
|
10322
|
+
return;
|
|
10323
|
+
}
|
|
10324
|
+
try {
|
|
10325
|
+
writeJson(res, 200, await materializeAdaptedBook(ctx, getConfig(), {
|
|
10326
|
+
text,
|
|
10327
|
+
bookName,
|
|
10328
|
+
outputDir: outDir,
|
|
10329
|
+
outline: body?.outline,
|
|
10330
|
+
proposal,
|
|
10331
|
+
chapterCount: body?.chapterCount
|
|
10332
|
+
}));
|
|
10333
|
+
} catch (err) {
|
|
10334
|
+
writeJson(res, 400, { error: err instanceof Error ? err.message : String(err) });
|
|
10335
|
+
}
|
|
10336
|
+
}
|
|
10337
|
+
};
|
|
10338
|
+
/** 保存预览/微调后的新书资料为新书(原书保留,登记书架)。 */
|
|
10339
|
+
const adaptMaterializeSaveRoute = {
|
|
10340
|
+
kind: "exact",
|
|
10341
|
+
path: NOVEL_API.adaptMaterializeSave,
|
|
10342
|
+
handler: async (req, res) => {
|
|
10343
|
+
if (!guard(req, res, "POST")) return;
|
|
10344
|
+
if (!getConfig().enableAdaptMode) {
|
|
10345
|
+
writeJson(res, 404, { error: "改编模式未开启" });
|
|
10346
|
+
return;
|
|
10347
|
+
}
|
|
10348
|
+
const body = await readJsonBody(req);
|
|
10349
|
+
if (body === void 0 || body === null || (body.outline ?? "").trim() === "") {
|
|
10350
|
+
writeJson(res, 400, { error: "请提供改编后总纲" });
|
|
10351
|
+
return;
|
|
10352
|
+
}
|
|
10353
|
+
const bookName = (body.bookName ?? "").trim().slice(0, 40) || "改编新书";
|
|
10354
|
+
const outDir = (body.outputDir ?? "").trim() !== "" && body.outputDir !== void 0 && body.outputDir.trim() !== "" ? body.outputDir.trim() : defaultOutputDirFor(bookName);
|
|
10355
|
+
const occupied = loadBookshelf().books.find((b) => b.outputDir === outDir);
|
|
10356
|
+
if (occupied !== void 0 && occupied.bookName !== bookName) {
|
|
10357
|
+
writeJson(res, 409, { error: `目录已被《${occupied.bookName}》占用,请更换书名或输出目录` });
|
|
10358
|
+
return;
|
|
10359
|
+
}
|
|
10360
|
+
try {
|
|
10361
|
+
const result = saveMaterializedBook(outDir, bookName, {
|
|
10362
|
+
outline: body.outline,
|
|
10363
|
+
bible: body.bible,
|
|
10364
|
+
roles: body.roles,
|
|
10365
|
+
world: body.world,
|
|
10366
|
+
volumes: body.volumes,
|
|
10367
|
+
chapters: body.chapters
|
|
10368
|
+
});
|
|
10369
|
+
const { book } = importDir(outDir);
|
|
10370
|
+
writeJson(res, 200, {
|
|
10371
|
+
...result,
|
|
10372
|
+
book
|
|
10373
|
+
});
|
|
10374
|
+
} catch (err) {
|
|
10375
|
+
writeJson(res, 400, { error: err instanceof Error ? err.message : String(err) });
|
|
10376
|
+
}
|
|
10377
|
+
}
|
|
10378
|
+
};
|
|
9739
10379
|
const THEME_BG_DIR = join(homedir(), ".dsh", "dsh-novel-forge-assets");
|
|
9740
10380
|
const MIME_BY_EXT = {
|
|
9741
10381
|
png: "image/png",
|
|
@@ -9794,6 +10434,7 @@ function makeRoutes(deps) {
|
|
|
9794
10434
|
chapterApproveRoute,
|
|
9795
10435
|
configRoute,
|
|
9796
10436
|
openFolderRoute,
|
|
10437
|
+
pluginUpdateRoute,
|
|
9797
10438
|
outlineSuggestRoute,
|
|
9798
10439
|
outlineReverseRoute,
|
|
9799
10440
|
manhuaPlansRoute,
|
|
@@ -9819,6 +10460,10 @@ function makeRoutes(deps) {
|
|
|
9819
10460
|
adaptAnalyzeRoute,
|
|
9820
10461
|
adaptProposeRoute,
|
|
9821
10462
|
adaptExecuteRoute,
|
|
10463
|
+
adaptSaveRoute,
|
|
10464
|
+
adaptMaterializeRoute,
|
|
10465
|
+
adaptMaterializeSaveRoute,
|
|
10466
|
+
adaptRewriteStreamRoute,
|
|
9822
10467
|
{
|
|
9823
10468
|
kind: "exact",
|
|
9824
10469
|
path: NOVEL_API.themeBackgroundUpload,
|