@waterwx/dsh-novel-forge 1.8.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 +3124 -1122
- package/lib/client.js.map +1 -1
- package/lib/index.js +1222 -179
- package/lib/index.js.map +1 -1
- package/lib/types/assets.d.ts +3 -1
- package/lib/types/client/api.d.ts +23 -1
- package/lib/types/client/locales.d.ts +16 -0
- package/lib/types/client/panel/AdaptModeView.d.ts +2 -1
- package/lib/types/client/panel/AuthorHome.d.ts +11 -1
- package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +4 -0
- package/lib/types/client/panel/ModelManager.d.ts +14 -0
- package/lib/types/client/panel/ModelPicker.d.ts +11 -0
- package/lib/types/client/panel/ProgressConsole.d.ts +17 -0
- package/lib/types/client/panel/ReasoningSection.d.ts +10 -0
- package/lib/types/client/panel/SettingsView.d.ts +1 -1
- package/lib/types/engine.d.ts +53 -1
- package/lib/types/protocol.d.ts +312 -4
- package/package.json +1 -1
- package/src/assets.ts +16 -1
- package/src/author-assets.ts +7 -26
- package/src/client/api.ts +102 -0
- package/src/client/locales.ts +32 -0
- package/src/client/panel/AdaptModeView.tsx +378 -62
- package/src/client/panel/AuthorAssetsView.tsx +3 -1
- package/src/client/panel/AuthorHome.tsx +130 -21
- package/src/client/panel/GlobalAssetLibraryView.tsx +172 -0
- package/src/client/panel/ModelManager.tsx +205 -0
- package/src/client/panel/NovelPanel.tsx +26 -63
- package/src/client/panel/ProgressConsole.tsx +64 -0
- package/src/client/panel/ProgressHomeView.tsx +1 -1
- package/src/client/panel/ReasoningSection.tsx +31 -0
- package/src/client/panel/SettingsView.tsx +56 -49
- package/src/client/panel/panel.module.css +25 -0
- package/src/engine.ts +455 -2
- package/src/index.ts +2 -2
- package/src/protocol.ts +321 -4
- package/src/routes.ts +338 -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 {
|
|
@@ -1186,6 +1337,215 @@ function normalizeEmotions(text) {
|
|
|
1186
1337
|
return out;
|
|
1187
1338
|
}
|
|
1188
1339
|
//#endregion
|
|
1340
|
+
//#region src/protocol.ts
|
|
1341
|
+
/** The /api/dsh-novel-forge route family (same-origin, loopback-fenced). */
|
|
1342
|
+
const NOVEL_API = {
|
|
1343
|
+
status: "/api/dsh-novel-forge/status",
|
|
1344
|
+
loadOutline: "/api/dsh-novel-forge/load-outline",
|
|
1345
|
+
saveOutline: "/api/dsh-novel-forge/save-outline",
|
|
1346
|
+
plan: "/api/dsh-novel-forge/plan",
|
|
1347
|
+
volumes: "/api/dsh-novel-forge/volumes",
|
|
1348
|
+
bible: "/api/dsh-novel-forge/bible",
|
|
1349
|
+
assets: "/api/dsh-novel-forge/assets",
|
|
1350
|
+
styleEngine: "/api/dsh-novel-forge/style-engine",
|
|
1351
|
+
generate: "/api/dsh-novel-forge/generate",
|
|
1352
|
+
review: "/api/dsh-novel-forge/review",
|
|
1353
|
+
rewrite: "/api/dsh-novel-forge/rewrite",
|
|
1354
|
+
polish: "/api/dsh-novel-forge/polish",
|
|
1355
|
+
/** 采纳待确认草稿(润色/重写产物)覆盖正文文件。 */
|
|
1356
|
+
draftApply: "/api/dsh-novel-forge/draft/apply",
|
|
1357
|
+
/** 放弃待确认草稿,保留原稿。 */
|
|
1358
|
+
draftDiscard: "/api/dsh-novel-forge/draft/discard",
|
|
1359
|
+
summary: "/api/dsh-novel-forge/summary",
|
|
1360
|
+
foreshadow: "/api/dsh-novel-forge/foreshadow",
|
|
1361
|
+
exportBook: "/api/dsh-novel-forge/export",
|
|
1362
|
+
chapter: "/api/dsh-novel-forge/chapter",
|
|
1363
|
+
/** 审查任意正文文本(作者手动编辑后,不落盘)。 */
|
|
1364
|
+
chapterCheck: "/api/dsh-novel-forge/chapter/check",
|
|
1365
|
+
/** 保存手动编辑的正文(自动备份 .bak)。 */
|
|
1366
|
+
chapterSave: "/api/dsh-novel-forge/chapter/save",
|
|
1367
|
+
assistant: "/api/dsh-novel-forge/assistant",
|
|
1368
|
+
assistantHistory: "/api/dsh-novel-forge/assistant-history",
|
|
1369
|
+
/** 清空助手对话记录。 */
|
|
1370
|
+
assistantClear: "/api/dsh-novel-forge/assistant/clear",
|
|
1371
|
+
bookshelf: "/api/dsh-novel-forge/bookshelf",
|
|
1372
|
+
/** 导入已有项目目录(含 novel-project.json)到书架。 */
|
|
1373
|
+
bookshelfImportDir: "/api/dsh-novel-forge/bookshelf/import-dir",
|
|
1374
|
+
/** 导入 txt/md 全本:拆章建项目并登记书架。 */
|
|
1375
|
+
bookshelfImportText: "/api/dsh-novel-forge/bookshelf/import-text",
|
|
1376
|
+
/** 导入 txt/md 全本:拆章预览(不落盘)。 */
|
|
1377
|
+
bookshelfImportTextPreview: "/api/dsh-novel-forge/bookshelf/import-text/preview",
|
|
1378
|
+
/** 漫剧方案管理:create/remove/activate。 */
|
|
1379
|
+
manhuaPlans: "/api/dsh-novel-forge/manhua/plans",
|
|
1380
|
+
/** 漫剧角色库:从分镜提名(规则+LLM两段式)/ 建卡 / 更新 / 删除 / 形象锚点 / 精修提示词。 */
|
|
1381
|
+
mangaRoles: "/api/dsh-novel-forge/manga/roles",
|
|
1382
|
+
/** 分镜·编剧级:单章 → 剧情骨架(节拍链)。 */
|
|
1383
|
+
storyboardSkeleton: "/api/dsh-novel-forge/storyboard/skeleton",
|
|
1384
|
+
/** 分镜·导演级:骨架 → 分镜表(镜头级)。 */
|
|
1385
|
+
storyboardTable: "/api/dsh-novel-forge/storyboard/table",
|
|
1386
|
+
/** 分镜·提示词级:分镜表 → 即梦可粘贴视频提示词。 */
|
|
1387
|
+
storyboardPrompts: "/api/dsh-novel-forge/storyboard/prompts",
|
|
1388
|
+
/** 生图接口连通性测试(设置页每个模型条目用)。 */
|
|
1389
|
+
imageTest: "/api/dsh-novel-forge/image-test",
|
|
1390
|
+
/** LLM 模型连通性测试:真实最小调用,验证 Key / 端点 / 模型可用。 */
|
|
1391
|
+
llmTest: "/api/dsh-novel-forge/llm-test",
|
|
1392
|
+
/** 添加模型:厂商直填 key 或自定义路由,写进 DSH 凭据与 llm-pi-ai 路由。 */
|
|
1393
|
+
addModel: "/api/dsh-novel-forge/llm-add",
|
|
1394
|
+
/** 运行时厂商目录(DSH pi-ai 可配置提供方 + 内置适配器)。 */
|
|
1395
|
+
llmVendors: "/api/dsh-novel-forge/llm-vendors",
|
|
1396
|
+
/** 查询某个 provider 当前可用的模型(添加成功后可即时刷新)。 */
|
|
1397
|
+
llmModels: "/api/dsh-novel-forge/llm-models",
|
|
1398
|
+
/** 已注册的提供方路由列表(提供方管理)。 */
|
|
1399
|
+
llmProviders: "/api/dsh-novel-forge/llm-providers",
|
|
1400
|
+
/** 移除一个提供方。 */
|
|
1401
|
+
llmRemove: "/api/dsh-novel-forge/llm-remove",
|
|
1402
|
+
/** 重置项目(可选携带新大纲):清空设定/卷/章节/伏笔/资产/事实库。 */
|
|
1403
|
+
reset: "/api/dsh-novel-forge/reset",
|
|
1404
|
+
/** 全书一致性质检:LLM 扫描已生成章节,输出矛盾问题清单。 */
|
|
1405
|
+
audit: "/api/dsh-novel-forge/audit",
|
|
1406
|
+
/** 角色卡刷新:基于事实库与各章摘要聚合角色当前状态。 */
|
|
1407
|
+
charactersRefresh: "/api/dsh-novel-forge/characters/refresh",
|
|
1408
|
+
/** 事实库回填:对历史已生成章节批量抽取事实(旧章节无事实记录时用)。 */
|
|
1409
|
+
factsBackfill: "/api/dsh-novel-forge/facts/backfill",
|
|
1410
|
+
/** 设定圣经局部修补(如世界观规则编辑)。 */
|
|
1411
|
+
biblePatch: "/api/dsh-novel-forge/bible/patch",
|
|
1412
|
+
/** 小说简介:生成(AI)/补全(AI)/保存。 */
|
|
1413
|
+
blurb: "/api/dsh-novel-forge/blurb",
|
|
1414
|
+
/** 重命名当前书(同步项目与书架条目)。 */
|
|
1415
|
+
rename: "/api/dsh-novel-forge/rename",
|
|
1416
|
+
/** 大世界:AI 提炼 / 保存结构化数据(境界/区域/势力)。 */
|
|
1417
|
+
world: "/api/dsh-novel-forge/world",
|
|
1418
|
+
/** 封面:GET 读取(dataUrl)/ POST 上传或移除。 */
|
|
1419
|
+
cover: "/api/dsh-novel-forge/blurb/cover",
|
|
1420
|
+
/** 剧情线管理:增删改 + 关联章节。 */
|
|
1421
|
+
plotlines: "/api/dsh-novel-forge/plotlines",
|
|
1422
|
+
/** 角色库:AI 提炼 / 采纳 / 更新 / 删除。 */
|
|
1423
|
+
roles: "/api/dsh-novel-forge/roles",
|
|
1424
|
+
scenes: "/api/dsh-novel-forge/scenes",
|
|
1425
|
+
visualRules: "/api/dsh-novel-forge/visual-rules",
|
|
1426
|
+
/** 作者复盘补跑:对已写章节补齐 authorReview(全书流式 / 单章 JSON)。 */
|
|
1427
|
+
reviewBackfill: "/api/dsh-novel-forge/review/backfill",
|
|
1428
|
+
/** 章节复位:generating 卡死 → pending(可重新生成)。 */
|
|
1429
|
+
chapterReset: "/api/dsh-novel-forge/chapter/reset",
|
|
1430
|
+
/** 章节直接通过:作者对 rejected/written 章节行使最终决定权。 */
|
|
1431
|
+
chapterApprove: "/api/dsh-novel-forge/chapter/approve",
|
|
1432
|
+
/** 敏感词检查:全书已写章节或指定文本。 */
|
|
1433
|
+
sensitiveCheck: "/api/dsh-novel-forge/sensitive-check",
|
|
1434
|
+
/** 开书想法 → AI 补全大纲:输入一句话想法,生成 2-3 个可选大纲方案。 */
|
|
1435
|
+
outlineSuggest: "/api/dsh-novel-forge/outline/suggest",
|
|
1436
|
+
/** 反推大纲:从已写章节正文反向生成全书总纲(NDJSON 流)。 */
|
|
1437
|
+
outlineReverse: "/api/dsh-novel-forge/outline/reverse",
|
|
1438
|
+
/** 拆书分析:对已写章节做结构/人物/文风/卖点四维体检(两阶段:源笔记→分节分析)。 */
|
|
1439
|
+
breakdown: "/api/dsh-novel-forge/breakdown",
|
|
1440
|
+
/** 漫剧分镜生成:章节 → 角色锚点 + 分镜表(可适配豆包/Seedance/SD)。 */
|
|
1441
|
+
/** 漫剧分集计划:读一卷 → 按故事弧线分集(高潮拆集/过渡并章)。 */
|
|
1442
|
+
/** 漫画脚本:章节 → 分页分格漫画脚本(含角色视觉锚点)。 */
|
|
1443
|
+
/** 生产单:启动批量生产(计划补足 + 逐章生成 + 被拒分级处理)。 */
|
|
1444
|
+
runStart: "/api/dsh-novel-forge/run/start",
|
|
1445
|
+
/** 生产单控制:pause / resume / stop。 */
|
|
1446
|
+
runControl: "/api/dsh-novel-forge/run/control",
|
|
1447
|
+
/** 生产单状态(含进度统计与日志)。 */
|
|
1448
|
+
runStatus: "/api/dsh-novel-forge/run/status",
|
|
1449
|
+
config: "/api/dsh-novel-forge/config",
|
|
1450
|
+
openFolder: "/api/dsh-novel-forge/open-folder",
|
|
1451
|
+
/** 插件自更新:在 DSH profile 目录拉取最新 npm 版(下载后需重启 DSH 生效)。 */
|
|
1452
|
+
pluginUpdate: "/api/dsh-novel-forge/plugin/update",
|
|
1453
|
+
/** 作者资产库/总数据:读取个人跨书资产(笔法/红线/套路/角色模板/世界观模板)。 */
|
|
1454
|
+
authorAssets: "/api/dsh-novel-forge/author-assets",
|
|
1455
|
+
/** 作者资产库:新增/更新一条资产(upsert by id)。 */
|
|
1456
|
+
authorAssetsUpsert: "/api/dsh-novel-forge/author-assets/upsert",
|
|
1457
|
+
/** 作者资产库:删除一条资产。 */
|
|
1458
|
+
authorAssetsRemove: "/api/dsh-novel-forge/author-assets/remove",
|
|
1459
|
+
/** 作者资产库:导入默认(书架书的写作资产/角色 + 内置全局库)批量沉淀。 */
|
|
1460
|
+
authorAssetsImportDefault: "/api/dsh-novel-forge/author-assets/import-default",
|
|
1461
|
+
/** 改编模式:上传全文 → 分析 → 原文设定卡片/可改范围矩阵。 */
|
|
1462
|
+
adaptAnalyze: "/api/dsh-novel-forge/adapt/analyze",
|
|
1463
|
+
/** 改编模式:确认要改的维度 → 生成映射表/改编规则/联动影响清单。 */
|
|
1464
|
+
adaptPropose: "/api/dsh-novel-forge/adapt/propose",
|
|
1465
|
+
/** 改编模式:执行术语替换(全局替换 + 命中统计 + 改编文本预览)。 */
|
|
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",
|
|
1475
|
+
/** 主题自定义背景:上传图片(POST,存盘并返回服务端 URL)。 */
|
|
1476
|
+
themeBackgroundUpload: "/api/dsh-novel-forge/theme/background",
|
|
1477
|
+
/** 主题自定义背景:读取已上传文件(GET prefix,/theme/background/<name>)。 */
|
|
1478
|
+
themeBackgroundGet: "/api/dsh-novel-forge/theme/background"
|
|
1479
|
+
};
|
|
1480
|
+
/** 预置的常见厂商(id=provider 路由;添加模型下拉兜底用,其余厂商由运行时目录动态补充)。 */
|
|
1481
|
+
const LLM_VENDORS = [
|
|
1482
|
+
{
|
|
1483
|
+
id: "deepseek-official",
|
|
1484
|
+
name: "DeepSeek",
|
|
1485
|
+
route: "deepseek-official",
|
|
1486
|
+
apiKeyEnv: "DEEPSEEK_API_KEY",
|
|
1487
|
+
defaultModel: "deepseek-v4-flash",
|
|
1488
|
+
models: [
|
|
1489
|
+
"deepseek-v4-flash",
|
|
1490
|
+
"deepseek-v4-pro",
|
|
1491
|
+
"deepseek-chat",
|
|
1492
|
+
"deepseek-reasoner"
|
|
1493
|
+
],
|
|
1494
|
+
builtin: true
|
|
1495
|
+
},
|
|
1496
|
+
{
|
|
1497
|
+
id: "zai-coding-cn",
|
|
1498
|
+
name: "智谱 GLM",
|
|
1499
|
+
route: "zai-coding-cn",
|
|
1500
|
+
apiKeyEnv: "ZAI_CODING_CN_API_KEY",
|
|
1501
|
+
defaultModel: "glm-5.3-flash",
|
|
1502
|
+
models: [
|
|
1503
|
+
"glm-4.5-air",
|
|
1504
|
+
"glm-4.7",
|
|
1505
|
+
"glm-5-turbo",
|
|
1506
|
+
"glm-5.1",
|
|
1507
|
+
"glm-5.2",
|
|
1508
|
+
"glm-5.3",
|
|
1509
|
+
"glm-5.3-flash",
|
|
1510
|
+
"glm-5v-turbo"
|
|
1511
|
+
]
|
|
1512
|
+
},
|
|
1513
|
+
{
|
|
1514
|
+
id: "qwen-token-plan-cn",
|
|
1515
|
+
name: "千问百炼",
|
|
1516
|
+
route: "qwen-token-plan-cn",
|
|
1517
|
+
apiKeyEnv: "QWEN_TOKEN_PLAN_CN_API_KEY",
|
|
1518
|
+
defaultModel: "qwen3.7-max",
|
|
1519
|
+
models: [
|
|
1520
|
+
"qwen3.6-flash",
|
|
1521
|
+
"qwen3.6-plus",
|
|
1522
|
+
"qwen3.7-max",
|
|
1523
|
+
"qwen3.7-plus",
|
|
1524
|
+
"qwen3.8-max",
|
|
1525
|
+
"qwen3.8-max-preview"
|
|
1526
|
+
]
|
|
1527
|
+
},
|
|
1528
|
+
{
|
|
1529
|
+
id: "openrouter",
|
|
1530
|
+
name: "OpenRouter",
|
|
1531
|
+
route: "openrouter",
|
|
1532
|
+
apiKeyEnv: "OPENROUTER_API_KEY",
|
|
1533
|
+
defaultModel: "z-ai/glm-5.3-flash",
|
|
1534
|
+
models: [
|
|
1535
|
+
"z-ai/glm-4.6",
|
|
1536
|
+
"z-ai/glm-4.7",
|
|
1537
|
+
"z-ai/glm-5",
|
|
1538
|
+
"z-ai/glm-5-turbo",
|
|
1539
|
+
"z-ai/glm-5.1",
|
|
1540
|
+
"z-ai/glm-5.2",
|
|
1541
|
+
"z-ai/glm-5.3-flash",
|
|
1542
|
+
"auto",
|
|
1543
|
+
"deepseek/deepseek-v4-flash",
|
|
1544
|
+
"anthropic/claude-sonnet-4.6"
|
|
1545
|
+
]
|
|
1546
|
+
}
|
|
1547
|
+
];
|
|
1548
|
+
//#endregion
|
|
1189
1549
|
//#region src/engine.ts
|
|
1190
1550
|
/**
|
|
1191
1551
|
* Novel engine — the host half's core: LLM-driven story-bible extraction,
|
|
@@ -3630,6 +3990,225 @@ async function testImageEndpoint(baseURL, apiKey, model) {
|
|
|
3630
3990
|
};
|
|
3631
3991
|
}
|
|
3632
3992
|
}
|
|
3993
|
+
/** LLM 连通性失败的错误码 → 人话(供设置页“测试连通”回显)。 */
|
|
3994
|
+
const LLM_TEST_ERROR_HINT = {
|
|
3995
|
+
NO_ADAPTER: "提供商路由不存在或未启用",
|
|
3996
|
+
UNKNOWN_MODEL: "模型不在该提供商的目录里",
|
|
3997
|
+
MISSING_CREDENTIAL: "API Key 未配置(检查 DSH 凭据里的引用)",
|
|
3998
|
+
INVALID_CREDENTIAL: "API Key 格式无效",
|
|
3999
|
+
AUTH: "认证失败:API Key 无效或无权限",
|
|
4000
|
+
RATE_LIMIT: "触发提供商限流,请稍后再试",
|
|
4001
|
+
QUOTA: "配额/余额不足",
|
|
4002
|
+
CONTEXT_WINDOW_EXCEEDED: "上下文超限(测试调用不应触发,请核实模型配置)",
|
|
4003
|
+
EMPTY_RESPONSE: "端点连通但返回空响应(模型可能暂不可用)",
|
|
4004
|
+
TIMEOUT: "连接超时",
|
|
4005
|
+
ABORTED: "测试超时(30 秒无响应)",
|
|
4006
|
+
UNSUPPORTED_REASONING_EFFORT: "推理档位不受此模型支持"
|
|
4007
|
+
};
|
|
4008
|
+
function describeLlmTestError(err) {
|
|
4009
|
+
const code = typeof err.code === "string" ? err.code : "";
|
|
4010
|
+
const hint = code !== "" ? LLM_TEST_ERROR_HINT[code] : void 0;
|
|
4011
|
+
const detail = err.message !== "" ? err.message : "未知错误";
|
|
4012
|
+
return hint !== void 0 ? `${hint}(${detail})` : detail;
|
|
4013
|
+
}
|
|
4014
|
+
/** 对选中的提供商/模型发一次最小真实调用(maxTokens=16),验证 Key / 端点 / 模型可用。 */
|
|
4015
|
+
async function testLlmModel(ctx, provider, model) {
|
|
4016
|
+
const start = Date.now();
|
|
4017
|
+
const request = {
|
|
4018
|
+
provider,
|
|
4019
|
+
model,
|
|
4020
|
+
messages: [createUserMessage({
|
|
4021
|
+
content: [{
|
|
4022
|
+
type: "text",
|
|
4023
|
+
text: "只回复两个字:OK"
|
|
4024
|
+
}],
|
|
4025
|
+
source: {
|
|
4026
|
+
kind: "plugin",
|
|
4027
|
+
plugin: "dsh-novel-forge"
|
|
4028
|
+
}
|
|
4029
|
+
})],
|
|
4030
|
+
maxTokens: 16,
|
|
4031
|
+
temperature: 0
|
|
4032
|
+
};
|
|
4033
|
+
const controller = new AbortController();
|
|
4034
|
+
const timer = setTimeout(() => controller.abort(), 3e4);
|
|
4035
|
+
try {
|
|
4036
|
+
const assembler = new BlockAssembler();
|
|
4037
|
+
let sawBlock = false;
|
|
4038
|
+
for await (const chunk of ctx.llm.stream({
|
|
4039
|
+
...request,
|
|
4040
|
+
signal: controller.signal
|
|
4041
|
+
})) {
|
|
4042
|
+
assembler.push(chunk);
|
|
4043
|
+
if (chunk.type === "block-end") {
|
|
4044
|
+
sawBlock = true;
|
|
4045
|
+
break;
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
if (sawBlock) return {
|
|
4049
|
+
ok: true,
|
|
4050
|
+
ms: Date.now() - start
|
|
4051
|
+
};
|
|
4052
|
+
const finish = assembler.finish;
|
|
4053
|
+
if (finish !== void 0 && (finish.kind === "error" || finish.kind === "aborted")) {
|
|
4054
|
+
const failure = finish.failure;
|
|
4055
|
+
throw Object.assign(new Error(failure.message), { code: failure.code });
|
|
4056
|
+
}
|
|
4057
|
+
return {
|
|
4058
|
+
ok: true,
|
|
4059
|
+
ms: Date.now() - start
|
|
4060
|
+
};
|
|
4061
|
+
} catch (error) {
|
|
4062
|
+
const err = error;
|
|
4063
|
+
return {
|
|
4064
|
+
ok: false,
|
|
4065
|
+
ms: Date.now() - start,
|
|
4066
|
+
code: err.code,
|
|
4067
|
+
message: describeLlmTestError(err)
|
|
4068
|
+
};
|
|
4069
|
+
} finally {
|
|
4070
|
+
clearTimeout(timer);
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
/** 确保 pi-ai 的一条 provider 路由存在(settings seam 深度合并,保留已有字段)。 */
|
|
4074
|
+
async function ensurePiAiProvider(ctx, route, cfg) {
|
|
4075
|
+
const settings = ctx.get("settings");
|
|
4076
|
+
if (settings === void 0) throw new Error("DSH settings 服务不可用,无法注册路由");
|
|
4077
|
+
await settings.update("llm-pi-ai", { providers: { [route]: cfg } });
|
|
4078
|
+
}
|
|
4079
|
+
/** 运行时厂商目录:DSH pi-ai 可配置提供方 + 内置适配器,作为「添加模型」下拉。 */
|
|
4080
|
+
async function listLlmVendors(ctx) {
|
|
4081
|
+
const map = /* @__PURE__ */ new Map();
|
|
4082
|
+
for (const v of LLM_VENDORS) map.set(v.route, {
|
|
4083
|
+
id: v.route,
|
|
4084
|
+
name: v.name,
|
|
4085
|
+
models: v.models,
|
|
4086
|
+
apiKeyEnv: v.apiKeyEnv,
|
|
4087
|
+
builtin: v.builtin
|
|
4088
|
+
});
|
|
4089
|
+
try {
|
|
4090
|
+
for (const p of ctx.llm.listConfigurableProviders()) if (!map.has(p.provider)) map.set(p.provider, {
|
|
4091
|
+
id: p.provider,
|
|
4092
|
+
name: p.displayName !== "" ? p.displayName : p.provider,
|
|
4093
|
+
models: [],
|
|
4094
|
+
apiKeyEnv: "PI_AI_" + p.provider.toUpperCase().replace(/[^A-Z0-9]+/g, "_") + "_API_KEY"
|
|
4095
|
+
});
|
|
4096
|
+
} catch {}
|
|
4097
|
+
try {
|
|
4098
|
+
for (const p of ctx.llm.listProviders()) if (!map.has(p.id)) map.set(p.id, {
|
|
4099
|
+
id: p.id,
|
|
4100
|
+
name: p.name !== "" && p.name !== p.id ? p.name : p.id,
|
|
4101
|
+
models: [],
|
|
4102
|
+
builtin: true
|
|
4103
|
+
});
|
|
4104
|
+
} catch {}
|
|
4105
|
+
return { vendors: [...map.values()] };
|
|
4106
|
+
}
|
|
4107
|
+
/** 查询某个 provider 当前可用模型(添加成功后可即时刷新下拉)。 */
|
|
4108
|
+
async function listLlmModels(ctx, provider) {
|
|
4109
|
+
if (provider.trim() === "") return { models: [] };
|
|
4110
|
+
try {
|
|
4111
|
+
return { models: (await ctx.llm.listModels(provider.trim())).map((m) => ({
|
|
4112
|
+
id: m.id,
|
|
4113
|
+
name: m.name
|
|
4114
|
+
})) };
|
|
4115
|
+
} catch {
|
|
4116
|
+
return { models: [] };
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
/** 当前已注册的提供方路由列表(提供方管理卡片)。 */
|
|
4120
|
+
async function listLlmProviders(ctx) {
|
|
4121
|
+
try {
|
|
4122
|
+
return { providers: ctx.llm.listProviders().map((p) => ({
|
|
4123
|
+
id: p.id,
|
|
4124
|
+
name: p.name !== "" && p.name !== p.id ? p.name : p.id
|
|
4125
|
+
})) };
|
|
4126
|
+
} catch {
|
|
4127
|
+
return { providers: [{
|
|
4128
|
+
id: "deepseek-official",
|
|
4129
|
+
name: "DeepSeek"
|
|
4130
|
+
}] };
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
/** 移除一个提供方:unset 凭据 ref + 移除 llm-pi-ai providers 路由。 */
|
|
4134
|
+
async function removeLlmProvider(ctx, req) {
|
|
4135
|
+
const provider = (req.provider ?? "").trim();
|
|
4136
|
+
if (provider === "") throw new Error("缺少 provider");
|
|
4137
|
+
if (provider === "deepseek-official") throw new Error("内置 DeepSeek 提供方不可删除");
|
|
4138
|
+
const creds = ctx.get("credentials");
|
|
4139
|
+
if (creds?.unset !== void 0 && req.apiKeyEnv !== void 0 && req.apiKeyEnv.trim() !== "") await creds.unset(req.apiKeyEnv.trim());
|
|
4140
|
+
const settings = ctx.get("settings");
|
|
4141
|
+
if (settings?.mutate !== void 0) await settings.mutate("llm-pi-ai", [{
|
|
4142
|
+
op: "unset",
|
|
4143
|
+
path: ["providers", provider]
|
|
4144
|
+
}]);
|
|
4145
|
+
return {
|
|
4146
|
+
ok: true,
|
|
4147
|
+
message: "已移除提供方 " + provider
|
|
4148
|
+
};
|
|
4149
|
+
}
|
|
4150
|
+
/**
|
|
4151
|
+
* 添加模型(DSH 同款体验):厂商直填 API Key,或自定义 OpenAI 兼容路由。
|
|
4152
|
+
* 写入 DSH 凭据 refs,并(必要时)注册/更新 llm-pi-ai provider 路由。
|
|
4153
|
+
*/
|
|
4154
|
+
async function registerLlmModel(ctx, req) {
|
|
4155
|
+
const apiKey = req.apiKey?.trim() ?? "";
|
|
4156
|
+
const model = req.model?.trim() ?? "";
|
|
4157
|
+
if (apiKey === "") throw new Error("API Key 不能为空");
|
|
4158
|
+
if (model === "") throw new Error("模型 id 不能为空");
|
|
4159
|
+
const creds = ctx.get("credentials");
|
|
4160
|
+
if (creds === void 0) throw new Error("DSH credentials 服务不可用,无法写入 API Key");
|
|
4161
|
+
let route;
|
|
4162
|
+
let env;
|
|
4163
|
+
let displayName;
|
|
4164
|
+
let message;
|
|
4165
|
+
if (req.mode === "vendor") {
|
|
4166
|
+
const vendorId = (req.vendor ?? "").trim();
|
|
4167
|
+
if (vendorId === "") throw new Error("请选择厂商");
|
|
4168
|
+
const v = LLM_VENDORS.find((x) => x.route === vendorId);
|
|
4169
|
+
route = vendorId;
|
|
4170
|
+
env = req.apiKeyEnv?.trim() || v?.apiKeyEnv || "PI_AI_" + vendorId.toUpperCase().replace(/[^A-Z0-9]+/g, "_") + "_API_KEY";
|
|
4171
|
+
displayName = v?.name ?? vendorId;
|
|
4172
|
+
const isBuiltin = v?.builtin === true || vendorId === "deepseek-official";
|
|
4173
|
+
await creds.set(env, apiKey);
|
|
4174
|
+
if (isBuiltin) message = "已写入 DSH 凭据(" + env + ")";
|
|
4175
|
+
else {
|
|
4176
|
+
await ensurePiAiProvider(ctx, route, { apiKeyEnv: env });
|
|
4177
|
+
message = "已写入 DSH 凭据并注册路由 " + route;
|
|
4178
|
+
}
|
|
4179
|
+
} else {
|
|
4180
|
+
route = (req.provider ?? "").trim();
|
|
4181
|
+
const baseURL = (req.baseURL ?? "").trim();
|
|
4182
|
+
if (route === "") throw new Error("自定义模式需填提供商路由 id");
|
|
4183
|
+
if (baseURL === "") throw new Error("自定义模式需填接口地址 (baseURL)");
|
|
4184
|
+
env = "NOVEL_CUSTOM_" + route.toUpperCase().replace(/[^A-Z0-9]+/g, "_") + "_API_KEY";
|
|
4185
|
+
displayName = req.name?.trim() || "Custom " + route;
|
|
4186
|
+
await creds.set(env, apiKey);
|
|
4187
|
+
await ensurePiAiProvider(ctx, route, {
|
|
4188
|
+
displayName,
|
|
4189
|
+
apiKeyEnv: env,
|
|
4190
|
+
api: "openai-completions",
|
|
4191
|
+
baseURL,
|
|
4192
|
+
models: [{ id: model }],
|
|
4193
|
+
compat: {
|
|
4194
|
+
supportsDeveloperRole: false,
|
|
4195
|
+
maxTokensField: "max_tokens"
|
|
4196
|
+
}
|
|
4197
|
+
});
|
|
4198
|
+
message = "已写入 DSH 凭据并注册路由 " + route;
|
|
4199
|
+
}
|
|
4200
|
+
return {
|
|
4201
|
+
ok: true,
|
|
4202
|
+
saved: {
|
|
4203
|
+
id: "saved-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 6),
|
|
4204
|
+
name: req.name?.trim() || displayName + " · " + model,
|
|
4205
|
+
provider: route,
|
|
4206
|
+
model
|
|
4207
|
+
},
|
|
4208
|
+
provider: route,
|
|
4209
|
+
message
|
|
4210
|
+
};
|
|
4211
|
+
}
|
|
3633
4212
|
/** 底层:按形象锚点调用生图接口生成定妆图(不写库;写库由上层调用方负责)。 */
|
|
3634
4213
|
async function generateReferenceImageFrom(config, anchor, style = "", modelId) {
|
|
3635
4214
|
if (config.imageApiEnabled !== true) throw new Error("生图未启用,请在设置中启用一个生图模型");
|
|
@@ -4624,30 +5203,286 @@ function normalizeAdaptationImpact(i) {
|
|
|
4624
5203
|
chapters: chapters !== void 0 && chapters.length > 0 ? chapters : void 0
|
|
4625
5204
|
};
|
|
4626
5205
|
}
|
|
4627
|
-
/**
|
|
4628
|
-
*
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
const
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
const
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
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
|
+
}
|
|
5462
|
+
/**
|
|
5463
|
+
* 摘要 + 事实抽取合并为一次 LLM 调用(省一次调用与一次正文输入,
|
|
5464
|
+
* 批量生成时整体开销约省 25%)。
|
|
5465
|
+
* @returns 摘要与新增事实条数(失败返回空,调用方 best-effort)。
|
|
5466
|
+
*/
|
|
5467
|
+
async function summarizeAndExtractFacts(ctx, config, project, outputDir, chapterNo) {
|
|
5468
|
+
const chapter = project.chapters.find((c) => c.no === chapterNo);
|
|
5469
|
+
if (chapter === void 0) return {
|
|
5470
|
+
summary: "",
|
|
5471
|
+
factCount: 0
|
|
5472
|
+
};
|
|
5473
|
+
const body = readChapterFile(outputDir, chapter);
|
|
5474
|
+
if (body === void 0) return {
|
|
5475
|
+
summary: "",
|
|
5476
|
+
factCount: 0
|
|
5477
|
+
};
|
|
5478
|
+
const raw = parseJsonObject(await complete(ctx, config, {
|
|
5479
|
+
system: [
|
|
5480
|
+
"你是一位网文编辑。请为下面一章做两件事,输出合法 JSON 对象:",
|
|
5481
|
+
"{\"summary\": \"120-200字摘要,含关键事件/主角状态变化(境界资源伤势心境)/新增伏笔线索/角色关系变化,客观陈述不评价\", \"facts\": [\"已确立事实1\", \"…3-6条\"]}",
|
|
5482
|
+
"facts 指:本章明确写出的、对后续有约束力的事实——人物当前状态、重要关系变化、地点与时间线、已落地或新增的伏笔线索、关键道具去向。",
|
|
5483
|
+
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。"
|
|
5484
|
+
].join("\n"),
|
|
5485
|
+
user: body.replace(/^#\s+.*$/m, "").trim(),
|
|
4651
5486
|
temperature: .2,
|
|
4652
5487
|
maxTokens: Math.max(config.maxTokens, 5e3)
|
|
4653
5488
|
}));
|
|
@@ -5660,125 +6495,6 @@ var ProductionRunner = class {
|
|
|
5660
6495
|
}
|
|
5661
6496
|
};
|
|
5662
6497
|
//#endregion
|
|
5663
|
-
//#region src/protocol.ts
|
|
5664
|
-
/** The /api/dsh-novel-forge route family (same-origin, loopback-fenced). */
|
|
5665
|
-
const NOVEL_API = {
|
|
5666
|
-
status: "/api/dsh-novel-forge/status",
|
|
5667
|
-
loadOutline: "/api/dsh-novel-forge/load-outline",
|
|
5668
|
-
saveOutline: "/api/dsh-novel-forge/save-outline",
|
|
5669
|
-
plan: "/api/dsh-novel-forge/plan",
|
|
5670
|
-
volumes: "/api/dsh-novel-forge/volumes",
|
|
5671
|
-
bible: "/api/dsh-novel-forge/bible",
|
|
5672
|
-
assets: "/api/dsh-novel-forge/assets",
|
|
5673
|
-
styleEngine: "/api/dsh-novel-forge/style-engine",
|
|
5674
|
-
generate: "/api/dsh-novel-forge/generate",
|
|
5675
|
-
review: "/api/dsh-novel-forge/review",
|
|
5676
|
-
rewrite: "/api/dsh-novel-forge/rewrite",
|
|
5677
|
-
polish: "/api/dsh-novel-forge/polish",
|
|
5678
|
-
/** 采纳待确认草稿(润色/重写产物)覆盖正文文件。 */
|
|
5679
|
-
draftApply: "/api/dsh-novel-forge/draft/apply",
|
|
5680
|
-
/** 放弃待确认草稿,保留原稿。 */
|
|
5681
|
-
draftDiscard: "/api/dsh-novel-forge/draft/discard",
|
|
5682
|
-
summary: "/api/dsh-novel-forge/summary",
|
|
5683
|
-
foreshadow: "/api/dsh-novel-forge/foreshadow",
|
|
5684
|
-
exportBook: "/api/dsh-novel-forge/export",
|
|
5685
|
-
chapter: "/api/dsh-novel-forge/chapter",
|
|
5686
|
-
/** 审查任意正文文本(作者手动编辑后,不落盘)。 */
|
|
5687
|
-
chapterCheck: "/api/dsh-novel-forge/chapter/check",
|
|
5688
|
-
/** 保存手动编辑的正文(自动备份 .bak)。 */
|
|
5689
|
-
chapterSave: "/api/dsh-novel-forge/chapter/save",
|
|
5690
|
-
assistant: "/api/dsh-novel-forge/assistant",
|
|
5691
|
-
assistantHistory: "/api/dsh-novel-forge/assistant-history",
|
|
5692
|
-
/** 清空助手对话记录。 */
|
|
5693
|
-
assistantClear: "/api/dsh-novel-forge/assistant/clear",
|
|
5694
|
-
bookshelf: "/api/dsh-novel-forge/bookshelf",
|
|
5695
|
-
/** 导入已有项目目录(含 novel-project.json)到书架。 */
|
|
5696
|
-
bookshelfImportDir: "/api/dsh-novel-forge/bookshelf/import-dir",
|
|
5697
|
-
/** 导入 txt/md 全本:拆章建项目并登记书架。 */
|
|
5698
|
-
bookshelfImportText: "/api/dsh-novel-forge/bookshelf/import-text",
|
|
5699
|
-
/** 导入 txt/md 全本:拆章预览(不落盘)。 */
|
|
5700
|
-
bookshelfImportTextPreview: "/api/dsh-novel-forge/bookshelf/import-text/preview",
|
|
5701
|
-
/** 漫剧方案管理:create/remove/activate。 */
|
|
5702
|
-
manhuaPlans: "/api/dsh-novel-forge/manhua/plans",
|
|
5703
|
-
/** 漫剧角色库:从分镜提名(规则+LLM两段式)/ 建卡 / 更新 / 删除 / 形象锚点 / 精修提示词。 */
|
|
5704
|
-
mangaRoles: "/api/dsh-novel-forge/manga/roles",
|
|
5705
|
-
/** 分镜·编剧级:单章 → 剧情骨架(节拍链)。 */
|
|
5706
|
-
storyboardSkeleton: "/api/dsh-novel-forge/storyboard/skeleton",
|
|
5707
|
-
/** 分镜·导演级:骨架 → 分镜表(镜头级)。 */
|
|
5708
|
-
storyboardTable: "/api/dsh-novel-forge/storyboard/table",
|
|
5709
|
-
/** 分镜·提示词级:分镜表 → 即梦可粘贴视频提示词。 */
|
|
5710
|
-
storyboardPrompts: "/api/dsh-novel-forge/storyboard/prompts",
|
|
5711
|
-
/** 生图接口连通性测试(设置页每个模型条目用)。 */
|
|
5712
|
-
imageTest: "/api/dsh-novel-forge/image-test",
|
|
5713
|
-
/** 重置项目(可选携带新大纲):清空设定/卷/章节/伏笔/资产/事实库。 */
|
|
5714
|
-
reset: "/api/dsh-novel-forge/reset",
|
|
5715
|
-
/** 全书一致性质检:LLM 扫描已生成章节,输出矛盾问题清单。 */
|
|
5716
|
-
audit: "/api/dsh-novel-forge/audit",
|
|
5717
|
-
/** 角色卡刷新:基于事实库与各章摘要聚合角色当前状态。 */
|
|
5718
|
-
charactersRefresh: "/api/dsh-novel-forge/characters/refresh",
|
|
5719
|
-
/** 事实库回填:对历史已生成章节批量抽取事实(旧章节无事实记录时用)。 */
|
|
5720
|
-
factsBackfill: "/api/dsh-novel-forge/facts/backfill",
|
|
5721
|
-
/** 设定圣经局部修补(如世界观规则编辑)。 */
|
|
5722
|
-
biblePatch: "/api/dsh-novel-forge/bible/patch",
|
|
5723
|
-
/** 小说简介:生成(AI)/补全(AI)/保存。 */
|
|
5724
|
-
blurb: "/api/dsh-novel-forge/blurb",
|
|
5725
|
-
/** 重命名当前书(同步项目与书架条目)。 */
|
|
5726
|
-
rename: "/api/dsh-novel-forge/rename",
|
|
5727
|
-
/** 大世界:AI 提炼 / 保存结构化数据(境界/区域/势力)。 */
|
|
5728
|
-
world: "/api/dsh-novel-forge/world",
|
|
5729
|
-
/** 封面:GET 读取(dataUrl)/ POST 上传或移除。 */
|
|
5730
|
-
cover: "/api/dsh-novel-forge/blurb/cover",
|
|
5731
|
-
/** 剧情线管理:增删改 + 关联章节。 */
|
|
5732
|
-
plotlines: "/api/dsh-novel-forge/plotlines",
|
|
5733
|
-
/** 角色库:AI 提炼 / 采纳 / 更新 / 删除。 */
|
|
5734
|
-
roles: "/api/dsh-novel-forge/roles",
|
|
5735
|
-
scenes: "/api/dsh-novel-forge/scenes",
|
|
5736
|
-
visualRules: "/api/dsh-novel-forge/visual-rules",
|
|
5737
|
-
/** 作者复盘补跑:对已写章节补齐 authorReview(全书流式 / 单章 JSON)。 */
|
|
5738
|
-
reviewBackfill: "/api/dsh-novel-forge/review/backfill",
|
|
5739
|
-
/** 章节复位:generating 卡死 → pending(可重新生成)。 */
|
|
5740
|
-
chapterReset: "/api/dsh-novel-forge/chapter/reset",
|
|
5741
|
-
/** 章节直接通过:作者对 rejected/written 章节行使最终决定权。 */
|
|
5742
|
-
chapterApprove: "/api/dsh-novel-forge/chapter/approve",
|
|
5743
|
-
/** 敏感词检查:全书已写章节或指定文本。 */
|
|
5744
|
-
sensitiveCheck: "/api/dsh-novel-forge/sensitive-check",
|
|
5745
|
-
/** 开书想法 → AI 补全大纲:输入一句话想法,生成 2-3 个可选大纲方案。 */
|
|
5746
|
-
outlineSuggest: "/api/dsh-novel-forge/outline/suggest",
|
|
5747
|
-
/** 反推大纲:从已写章节正文反向生成全书总纲(NDJSON 流)。 */
|
|
5748
|
-
outlineReverse: "/api/dsh-novel-forge/outline/reverse",
|
|
5749
|
-
/** 拆书分析:对已写章节做结构/人物/文风/卖点四维体检(两阶段:源笔记→分节分析)。 */
|
|
5750
|
-
breakdown: "/api/dsh-novel-forge/breakdown",
|
|
5751
|
-
/** 漫剧分镜生成:章节 → 角色锚点 + 分镜表(可适配豆包/Seedance/SD)。 */
|
|
5752
|
-
/** 漫剧分集计划:读一卷 → 按故事弧线分集(高潮拆集/过渡并章)。 */
|
|
5753
|
-
/** 漫画脚本:章节 → 分页分格漫画脚本(含角色视觉锚点)。 */
|
|
5754
|
-
/** 生产单:启动批量生产(计划补足 + 逐章生成 + 被拒分级处理)。 */
|
|
5755
|
-
runStart: "/api/dsh-novel-forge/run/start",
|
|
5756
|
-
/** 生产单控制:pause / resume / stop。 */
|
|
5757
|
-
runControl: "/api/dsh-novel-forge/run/control",
|
|
5758
|
-
/** 生产单状态(含进度统计与日志)。 */
|
|
5759
|
-
runStatus: "/api/dsh-novel-forge/run/status",
|
|
5760
|
-
config: "/api/dsh-novel-forge/config",
|
|
5761
|
-
openFolder: "/api/dsh-novel-forge/open-folder",
|
|
5762
|
-
/** 作者资产库/总数据:读取个人跨书资产(笔法/红线/套路/角色模板/世界观模板)。 */
|
|
5763
|
-
authorAssets: "/api/dsh-novel-forge/author-assets",
|
|
5764
|
-
/** 作者资产库:新增/更新一条资产(upsert by id)。 */
|
|
5765
|
-
authorAssetsUpsert: "/api/dsh-novel-forge/author-assets/upsert",
|
|
5766
|
-
/** 作者资产库:删除一条资产。 */
|
|
5767
|
-
authorAssetsRemove: "/api/dsh-novel-forge/author-assets/remove",
|
|
5768
|
-
/** 作者资产库:导入默认(书架书的写作资产/角色 + 内置全局库)批量沉淀。 */
|
|
5769
|
-
authorAssetsImportDefault: "/api/dsh-novel-forge/author-assets/import-default",
|
|
5770
|
-
/** 改编模式:上传全文 → 分析 → 原文设定卡片/可改范围矩阵。 */
|
|
5771
|
-
adaptAnalyze: "/api/dsh-novel-forge/adapt/analyze",
|
|
5772
|
-
/** 改编模式:确认要改的维度 → 生成映射表/改编规则/联动影响清单。 */
|
|
5773
|
-
adaptPropose: "/api/dsh-novel-forge/adapt/propose",
|
|
5774
|
-
/** 改编模式:执行术语替换(全局替换 + 命中统计 + 改编文本预览)。 */
|
|
5775
|
-
adaptExecute: "/api/dsh-novel-forge/adapt/execute",
|
|
5776
|
-
/** 主题自定义背景:上传图片(POST,存盘并返回服务端 URL)。 */
|
|
5777
|
-
themeBackgroundUpload: "/api/dsh-novel-forge/theme/background",
|
|
5778
|
-
/** 主题自定义背景:读取已上传文件(GET prefix,/theme/background/<name>)。 */
|
|
5779
|
-
themeBackgroundGet: "/api/dsh-novel-forge/theme/background"
|
|
5780
|
-
};
|
|
5781
|
-
//#endregion
|
|
5782
6498
|
//#region src/docx.ts
|
|
5783
6499
|
/**
|
|
5784
6500
|
* docx outline extraction: a .docx is a zip whose word/document.xml holds the
|
|
@@ -6664,23 +7380,14 @@ function importDefaultAuthorAssets() {
|
|
|
6664
7380
|
updatedAt: now
|
|
6665
7381
|
});
|
|
6666
7382
|
};
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
});
|
|
6676
|
-
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(";"), ["内置", "风格模板"], [], {
|
|
6677
|
-
category: t.category,
|
|
6678
|
-
applicableGenres: t.applicableGenres
|
|
6679
|
-
});
|
|
6680
|
-
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(";"), ["内置", "推进"], [], {
|
|
6681
|
-
driver: p.driver,
|
|
6682
|
-
primary: p.primary
|
|
6683
|
-
});
|
|
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));
|
|
6684
7391
|
const shelf = loadBookshelf();
|
|
6685
7392
|
for (const book of shelf.books) {
|
|
6686
7393
|
const project = loadProject(book.outputDir);
|
|
@@ -6705,7 +7412,8 @@ function importDefaultAuthorAssets() {
|
|
|
6705
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 });
|
|
6706
7413
|
}
|
|
6707
7414
|
for (const role of project.roles ?? []) {
|
|
6708
|
-
|
|
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();
|
|
6709
7417
|
push("roleTemplate", role.name, role.identity, persona, src, [], {
|
|
6710
7418
|
roleLabel: role.roleLabel,
|
|
6711
7419
|
traits: role.traits,
|
|
@@ -6718,18 +7426,6 @@ function importDefaultAuthorAssets() {
|
|
|
6718
7426
|
saveAuthorAssets(library);
|
|
6719
7427
|
return library;
|
|
6720
7428
|
}
|
|
6721
|
-
/** 拍平题材库(含子题材,父级名称并入 tag)。 */
|
|
6722
|
-
function flattenGenres(nodes, parentName) {
|
|
6723
|
-
const out = [];
|
|
6724
|
-
for (const n of nodes) {
|
|
6725
|
-
out.push({
|
|
6726
|
-
...n,
|
|
6727
|
-
name: parentName !== void 0 ? parentName + "/" + n.name : n.name
|
|
6728
|
-
});
|
|
6729
|
-
if (n.children.length > 0) out.push(...flattenGenres(n.children, parentName !== void 0 ? parentName + "/" + n.name : n.name));
|
|
6730
|
-
}
|
|
6731
|
-
return out;
|
|
6732
|
-
}
|
|
6733
7429
|
//#endregion
|
|
6734
7430
|
//#region src/routes.ts
|
|
6735
7431
|
/** Cap on JSON request bodies (generous: cover images travel as base64). */
|
|
@@ -7604,7 +8300,8 @@ function makeRoutes(deps) {
|
|
|
7604
8300
|
genreLibrary: BUILTIN_GENRE_LIBRARY,
|
|
7605
8301
|
antiAiLibrary: BUILTIN_ANTI_AI_RULES,
|
|
7606
8302
|
styleTemplates: BUILTIN_STYLE_TEMPLATES,
|
|
7607
|
-
progressionLibrary: BUILTIN_PROGRESSION_MODES
|
|
8303
|
+
progressionLibrary: BUILTIN_PROGRESSION_MODES,
|
|
8304
|
+
plotBeatLibrary: BUILTIN_PLOT_BEATS
|
|
7608
8305
|
});
|
|
7609
8306
|
}
|
|
7610
8307
|
};
|
|
@@ -8805,6 +9502,42 @@ function makeRoutes(deps) {
|
|
|
8805
9502
|
});
|
|
8806
9503
|
}
|
|
8807
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
|
+
};
|
|
8808
9541
|
/** 开书想法 → AI 大纲:2-3 个可选方案(支持暂留换批:count 只补空槽 + exclude 避开已留方向)。 */
|
|
8809
9542
|
const outlineSuggestRoute = {
|
|
8810
9543
|
kind: "exact",
|
|
@@ -8917,6 +9650,99 @@ function makeRoutes(deps) {
|
|
|
8917
9650
|
}
|
|
8918
9651
|
}
|
|
8919
9652
|
};
|
|
9653
|
+
/** LLM 连通性测试:对选中的提供商/模型发一次最小真实调用。 */
|
|
9654
|
+
const llmTestRoute = {
|
|
9655
|
+
kind: "exact",
|
|
9656
|
+
path: NOVEL_API.llmTest,
|
|
9657
|
+
handler: async (req, res) => {
|
|
9658
|
+
if (!guard(req, res, "POST")) return;
|
|
9659
|
+
const body = await readJsonBody(req);
|
|
9660
|
+
const provider = (body?.provider ?? "").trim();
|
|
9661
|
+
const model = (body?.model ?? "").trim();
|
|
9662
|
+
if (provider === "" || model === "") {
|
|
9663
|
+
writeJson(res, 400, { error: "provider 与 model 必填" });
|
|
9664
|
+
return;
|
|
9665
|
+
}
|
|
9666
|
+
writeJson(res, 200, await testLlmModel(ctx, provider, model));
|
|
9667
|
+
}
|
|
9668
|
+
};
|
|
9669
|
+
/** 添加模型:厂商直填 API Key,或自定义 OpenAI 兼容路由(写 DSH 凭据 + router)。 */
|
|
9670
|
+
const addModelRoute = {
|
|
9671
|
+
kind: "exact",
|
|
9672
|
+
path: NOVEL_API.addModel,
|
|
9673
|
+
handler: async (req, res) => {
|
|
9674
|
+
if (!guard(req, res, "POST")) return;
|
|
9675
|
+
const body = await readJsonBody(req);
|
|
9676
|
+
if (body === void 0) {
|
|
9677
|
+
writeJson(res, 400, { error: "缺少请求体" });
|
|
9678
|
+
return;
|
|
9679
|
+
}
|
|
9680
|
+
try {
|
|
9681
|
+
writeJson(res, 200, await registerLlmModel(ctx, body));
|
|
9682
|
+
} catch (error) {
|
|
9683
|
+
writeJson(res, 500, { error: `添加模型失败:${error.message}` });
|
|
9684
|
+
}
|
|
9685
|
+
}
|
|
9686
|
+
};
|
|
9687
|
+
/** 运行时厂商目录(DSH pi-ai 可配置提供方 + 内置适配器)。 */
|
|
9688
|
+
const llmVendorsRoute = {
|
|
9689
|
+
kind: "exact",
|
|
9690
|
+
path: NOVEL_API.llmVendors,
|
|
9691
|
+
handler: async (req, res) => {
|
|
9692
|
+
if (!guard(req, res, "GET")) return;
|
|
9693
|
+
try {
|
|
9694
|
+
writeJson(res, 200, await listLlmVendors(ctx));
|
|
9695
|
+
} catch (error) {
|
|
9696
|
+
writeJson(res, 500, { error: `读取厂商目录失败:${error.message}` });
|
|
9697
|
+
}
|
|
9698
|
+
}
|
|
9699
|
+
};
|
|
9700
|
+
/** 查询某个 provider 当前可用的模型(添加成功后可即时刷新下拉)。 */
|
|
9701
|
+
const llmModelsRoute = {
|
|
9702
|
+
kind: "exact",
|
|
9703
|
+
path: NOVEL_API.llmModels,
|
|
9704
|
+
handler: async (req, res) => {
|
|
9705
|
+
if (!guard(req, res, "GET")) return;
|
|
9706
|
+
const url = new URL(req.url ?? "", "http://localhost");
|
|
9707
|
+
const provider = decodeURIComponent((url.searchParams.get("provider") ?? "").trim());
|
|
9708
|
+
try {
|
|
9709
|
+
writeJson(res, 200, await listLlmModels(ctx, provider));
|
|
9710
|
+
} catch (error) {
|
|
9711
|
+
writeJson(res, 500, { error: `读取模型列表失败:${error.message}` });
|
|
9712
|
+
}
|
|
9713
|
+
}
|
|
9714
|
+
};
|
|
9715
|
+
/** 已注册的提供方路由列表(提供方管理卡片)。 */
|
|
9716
|
+
const llmProvidersRoute = {
|
|
9717
|
+
kind: "exact",
|
|
9718
|
+
path: NOVEL_API.llmProviders,
|
|
9719
|
+
handler: async (req, res) => {
|
|
9720
|
+
if (!guard(req, res, "GET")) return;
|
|
9721
|
+
try {
|
|
9722
|
+
writeJson(res, 200, await listLlmProviders(ctx));
|
|
9723
|
+
} catch (error) {
|
|
9724
|
+
writeJson(res, 500, { error: `读取提供方列表失败:${error.message}` });
|
|
9725
|
+
}
|
|
9726
|
+
}
|
|
9727
|
+
};
|
|
9728
|
+
/** 移除一个提供方(unset key + 移除 llm-pi-ai 路由)。 */
|
|
9729
|
+
const llmRemoveRoute = {
|
|
9730
|
+
kind: "exact",
|
|
9731
|
+
path: NOVEL_API.llmRemove,
|
|
9732
|
+
handler: async (req, res) => {
|
|
9733
|
+
if (!guard(req, res, "POST")) return;
|
|
9734
|
+
const body = await readJsonBody(req);
|
|
9735
|
+
if (body === void 0) {
|
|
9736
|
+
writeJson(res, 400, { error: "缺少请求体" });
|
|
9737
|
+
return;
|
|
9738
|
+
}
|
|
9739
|
+
try {
|
|
9740
|
+
writeJson(res, 200, await removeLlmProvider(ctx, body));
|
|
9741
|
+
} catch (error) {
|
|
9742
|
+
writeJson(res, 500, { error: `移除提供方失败:${error.message}` });
|
|
9743
|
+
}
|
|
9744
|
+
}
|
|
9745
|
+
};
|
|
8920
9746
|
/** 分镜·导演级:骨架 → 分镜表(镜头级)。 */
|
|
8921
9747
|
const storyboardTableRoute = {
|
|
8922
9748
|
kind: "exact",
|
|
@@ -9333,17 +10159,223 @@ function makeRoutes(deps) {
|
|
|
9333
10159
|
return;
|
|
9334
10160
|
}
|
|
9335
10161
|
if (body?.mode === "rewrite") {
|
|
9336
|
-
|
|
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
|
+
}
|
|
9337
10179
|
return;
|
|
9338
10180
|
}
|
|
9339
10181
|
const { adaptedText, hits } = applyAdaptationReplacements(text, mappings);
|
|
9340
10182
|
writeJson(res, 200, {
|
|
9341
10183
|
adaptedText,
|
|
9342
10184
|
mappings: mappings.length,
|
|
9343
|
-
hits
|
|
10185
|
+
hits,
|
|
10186
|
+
mode: "replace"
|
|
9344
10187
|
});
|
|
9345
10188
|
}
|
|
9346
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
|
+
};
|
|
9347
10379
|
const THEME_BG_DIR = join(homedir(), ".dsh", "dsh-novel-forge-assets");
|
|
9348
10380
|
const MIME_BY_EXT = {
|
|
9349
10381
|
png: "image/png",
|
|
@@ -9402,12 +10434,19 @@ function makeRoutes(deps) {
|
|
|
9402
10434
|
chapterApproveRoute,
|
|
9403
10435
|
configRoute,
|
|
9404
10436
|
openFolderRoute,
|
|
10437
|
+
pluginUpdateRoute,
|
|
9405
10438
|
outlineSuggestRoute,
|
|
9406
10439
|
outlineReverseRoute,
|
|
9407
10440
|
manhuaPlansRoute,
|
|
9408
10441
|
styleImageRoute,
|
|
9409
10442
|
storyboardSkeletonRoute,
|
|
9410
10443
|
imageTestRoute,
|
|
10444
|
+
llmTestRoute,
|
|
10445
|
+
addModelRoute,
|
|
10446
|
+
llmVendorsRoute,
|
|
10447
|
+
llmModelsRoute,
|
|
10448
|
+
llmProvidersRoute,
|
|
10449
|
+
llmRemoveRoute,
|
|
9411
10450
|
storyboardTableRoute,
|
|
9412
10451
|
storyboardPromptsRoute,
|
|
9413
10452
|
breakdownRoute,
|
|
@@ -9421,6 +10460,10 @@ function makeRoutes(deps) {
|
|
|
9421
10460
|
adaptAnalyzeRoute,
|
|
9422
10461
|
adaptProposeRoute,
|
|
9423
10462
|
adaptExecuteRoute,
|
|
10463
|
+
adaptSaveRoute,
|
|
10464
|
+
adaptMaterializeRoute,
|
|
10465
|
+
adaptMaterializeSaveRoute,
|
|
10466
|
+
adaptRewriteStreamRoute,
|
|
9424
10467
|
{
|
|
9425
10468
|
kind: "exact",
|
|
9426
10469
|
path: NOVEL_API.themeBackgroundUpload,
|
|
@@ -9537,7 +10580,7 @@ const Config = z.object({
|
|
|
9537
10580
|
themeBackground: z.string().default(""),
|
|
9538
10581
|
themeBackgroundBlur: z.number().default(0),
|
|
9539
10582
|
themeOpacity: z.number().default(100),
|
|
9540
|
-
enableAdaptMode: z.boolean().default(
|
|
10583
|
+
enableAdaptMode: z.boolean().default(true)
|
|
9541
10584
|
});
|
|
9542
10585
|
/** Schema defaults, re-read for hand-built test contexts. */
|
|
9543
10586
|
const DEFAULT_ANNOUNCE = true;
|
|
@@ -9594,7 +10637,7 @@ function resolveConfig(value) {
|
|
|
9594
10637
|
themeBackground: value?.themeBackground ?? "",
|
|
9595
10638
|
themeBackgroundBlur: value?.themeBackgroundBlur ?? 0,
|
|
9596
10639
|
themeOpacity: value?.themeOpacity ?? 100,
|
|
9597
|
-
enableAdaptMode: value?.enableAdaptMode ??
|
|
10640
|
+
enableAdaptMode: value?.enableAdaptMode ?? true
|
|
9598
10641
|
};
|
|
9599
10642
|
}
|
|
9600
10643
|
/** 生图默认接口地址(豆包 ark,OpenAI 兼容)。 */
|