@waterwx/dsh-novel-forge 1.7.3 → 1.8.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/lib/client.js +3219 -662
- package/lib/client.js.map +1 -1
- package/lib/index.js +629 -41
- package/lib/index.js.map +1 -1
- package/lib/types/author-assets.d.ts +25 -0
- package/lib/types/client/api.d.ts +18 -0
- package/lib/types/client/panel/AdaptModeView.d.ts +4 -0
- package/lib/types/client/panel/AuthorAssetsView.d.ts +4 -0
- package/lib/types/client/panel/AuthorHome.d.ts +20 -0
- package/lib/types/client/panel/ProgressHomeView.d.ts +5 -0
- package/lib/types/client/panel/SettingsView.d.ts +14 -0
- package/lib/types/client/text.d.ts +9 -0
- package/lib/types/engine.d.ts +24 -1
- package/lib/types/index.d.ts +8 -0
- package/lib/types/protocol.d.ts +192 -0
- package/package.json +1 -1
- package/src/author-assets.ts +166 -0
- package/src/client/api.ts +41 -0
- package/src/client/mount.tsx._new +97 -0
- package/src/client/mount.tsx._nf +97 -0
- package/src/client/mount.tsx._tmp +97 -0
- package/src/client/panel/AdaptModeView.tsx +204 -0
- package/src/client/panel/AuthorAssetsView.tsx +206 -0
- package/src/client/panel/AuthorHome.tsx +93 -0
- package/src/client/panel/ImportModal.tsx +2 -1
- package/src/client/panel/NovelPanel.tsx +47 -16
- package/src/client/panel/ProgressHomeView.tsx +87 -0
- package/src/client/panel/SettingsView.tsx +285 -0
- package/src/client/panel/panel.module.css +325 -0
- package/src/client/sidebar-entry.ts +1 -1
- package/src/client/text.ts +28 -0
- package/src/engine.ts +221 -0
- package/src/index.ts +20 -0
- package/src/protocol.ts +196 -0
- package/src/routes.ts +191 -0
package/lib/index.js
CHANGED
|
@@ -4,9 +4,9 @@ import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-sett
|
|
|
4
4
|
import z from "schemastery";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { randomBytes } from "node:crypto";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { BlockAssembler, ReasoningEffortId, createAssistantMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
9
|
-
import { randomBytes } from "node:crypto";
|
|
10
10
|
import { strFromU8, unzipSync } from "fflate";
|
|
11
11
|
//#region src/assets.ts
|
|
12
12
|
/** 预置写法模板(来自 AI-Novel-Writing-Assistant 内置 DEFAULT_STYLE_TEMPLATES)。 */
|
|
@@ -4422,6 +4422,209 @@ async function reverseOutlineFromChapters(ctx, config, project, outputDir, onPro
|
|
|
4422
4422
|
return outline.trim();
|
|
4423
4423
|
}
|
|
4424
4424
|
/**
|
|
4425
|
+
* 改编模式 P0:全文分析 → 原文设定卡片 / 可改范围矩阵。
|
|
4426
|
+
* 拆章统计 + 取样正文,让 LLM 一次输出结构化 JSON:
|
|
4427
|
+
* { bookName, outline, dimensions: [{key,title,mutability,current,evidence,candidates,impact,risk}] }。
|
|
4428
|
+
*/
|
|
4429
|
+
async function analyzeAdaptation(ctx, config, text) {
|
|
4430
|
+
const chapters = splitBookText(text).filter((c) => c.body.length >= 50);
|
|
4431
|
+
if (chapters.length === 0) throw new Error("未能从全文拆出章节(内容过短或无章节结构)");
|
|
4432
|
+
const sampleBodies = [];
|
|
4433
|
+
const n = chapters.length;
|
|
4434
|
+
const pick = (i) => {
|
|
4435
|
+
const c = chapters[i];
|
|
4436
|
+
if (c !== void 0) sampleBodies.push("第" + c.no + "章《" + (c.title || "无题") + "》\n" + c.body.slice(0, 1200));
|
|
4437
|
+
};
|
|
4438
|
+
pick(0);
|
|
4439
|
+
if (n > 3) {
|
|
4440
|
+
pick(Math.floor(n / 3));
|
|
4441
|
+
pick(Math.floor(2 * n / 3));
|
|
4442
|
+
}
|
|
4443
|
+
if (n > 1) pick(n - 1);
|
|
4444
|
+
const raw = parseJsonObject(await complete(ctx, config, {
|
|
4445
|
+
system: "你是改编策划分析助手。",
|
|
4446
|
+
user: [
|
|
4447
|
+
"你是一位资深网文编辑兼改编策划。下面给你一部已完结/连载小说的若干章正文节选。请通读并输出该书的「原文设定卡片」与「可改范围矩阵」。",
|
|
4448
|
+
"输出合法 JSON 对象:",
|
|
4449
|
+
"{\"bookName\": \"书名\", \"outline\": \"一句话主线梗概(100字内)\", \"dimensions\": [{\"key\":\"realm\",\"title\":\"大世界\",\"mutability\":\"big|small|free|locked|visual\",\"current\":\"当前值\",\"evidence\":\"证据(出现章节/频次)\",\"candidates\":[\"候选1\",\"候选2\"],\"impact\":\"改了会影响什么\",\"risk\":\"high|medium|low\"}]}",
|
|
4450
|
+
"dimensions 至少覆盖以下维度(key/title):realm 大世界、cultivation 修为体系、protagonist 主角、goldenFinger 金手指、supporting 配角与势力人物名、faction 势力/组织、style 文风与叙事、ending 结局走向、timeline 时间线/编年、foreshadow 伏笔/暗线。",
|
|
4451
|
+
"mutability 取值:locked=建议保留、big=可改影响大、small=可改影响小、free=可自由改、visual=仅视觉包装。",
|
|
4452
|
+
"每个 dimension.current 必须忠于文本,能引用原文就用原文(尤其角色名/境界名/势力名/金手指名)。",
|
|
4453
|
+
"重要:所有字符串值内部不得包含换行符;JSON 必须在一段内完整结束;直接输出 JSON,不要 Markdown 代码块。"
|
|
4454
|
+
].join("\n") + "\n\n" + sampleBodies.join("\n\n---\n\n"),
|
|
4455
|
+
temperature: .3,
|
|
4456
|
+
maxTokens: Math.max(config.maxTokens, 6e3),
|
|
4457
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
4458
|
+
}));
|
|
4459
|
+
const bookName = typeof raw.bookName === "string" ? raw.bookName.trim() : "未命名";
|
|
4460
|
+
const dims = Array.isArray(raw.dimensions) ? raw.dimensions.filter((d) => typeof d === "object" && d !== null).map((d) => normalizeAdaptationDimension(d)).filter((d) => d !== null) : [];
|
|
4461
|
+
let outline;
|
|
4462
|
+
try {
|
|
4463
|
+
outline = await reverseOutlineFromAdaptationText(ctx, config, chapters);
|
|
4464
|
+
} catch {
|
|
4465
|
+
outline = typeof raw.outline === "string" && raw.outline.trim() !== "" ? raw.outline.trim() : void 0;
|
|
4466
|
+
}
|
|
4467
|
+
return {
|
|
4468
|
+
bookName,
|
|
4469
|
+
chapters: chapters.length,
|
|
4470
|
+
outline,
|
|
4471
|
+
dimensions: dims,
|
|
4472
|
+
note: "基于节选分析(首/中/末取样)+ 反推大纲。如需逐章全文级深度分析请后续启用全文流式入口。"
|
|
4473
|
+
};
|
|
4474
|
+
}
|
|
4475
|
+
/** 校验并归一化一行的改编维度数据(来自 LLM)。 */
|
|
4476
|
+
function normalizeAdaptationDimension(d) {
|
|
4477
|
+
const key = typeof d.key === "string" ? d.key : "";
|
|
4478
|
+
const title = typeof d.title === "string" ? d.title : "";
|
|
4479
|
+
if (key === "" || title === "") return null;
|
|
4480
|
+
const mutability = [
|
|
4481
|
+
"locked",
|
|
4482
|
+
"big",
|
|
4483
|
+
"small",
|
|
4484
|
+
"free",
|
|
4485
|
+
"visual"
|
|
4486
|
+
].includes(d.mutability) ? d.mutability : "small";
|
|
4487
|
+
const risk = [
|
|
4488
|
+
"high",
|
|
4489
|
+
"medium",
|
|
4490
|
+
"low"
|
|
4491
|
+
].includes(d.risk) ? d.risk : "medium";
|
|
4492
|
+
return {
|
|
4493
|
+
key,
|
|
4494
|
+
title,
|
|
4495
|
+
mutability,
|
|
4496
|
+
current: typeof d.current === "string" ? d.current : "",
|
|
4497
|
+
evidence: typeof d.evidence === "string" ? d.evidence : void 0,
|
|
4498
|
+
candidates: Array.isArray(d.candidates) ? d.candidates.filter((x) => typeof x === "string") : [],
|
|
4499
|
+
impact: typeof d.impact === "string" ? d.impact : "",
|
|
4500
|
+
risk
|
|
4501
|
+
};
|
|
4502
|
+
}
|
|
4503
|
+
/** 从全文拆出的章节节选反推全书总纲(Markdown),用于改编 P0 的「反推大纲」。 */
|
|
4504
|
+
async function reverseOutlineFromAdaptationText(ctx, config, chapters) {
|
|
4505
|
+
const n = chapters.length;
|
|
4506
|
+
const sample = [];
|
|
4507
|
+
const count = Math.min(n, 20);
|
|
4508
|
+
for (let i = 0; i < count; i++) {
|
|
4509
|
+
const c = chapters[Math.floor(i * n / count)];
|
|
4510
|
+
if (c === void 0) continue;
|
|
4511
|
+
sample.push("第" + c.no + "章《" + (c.title || "无题") + "》\n" + c.body.slice(0, 600));
|
|
4512
|
+
}
|
|
4513
|
+
return (await complete(ctx, config, {
|
|
4514
|
+
system: [
|
|
4515
|
+
"你是一位经验丰富的小说主编。根据下面若干章节的正文节选,反推出这本书的总纲(可作为后续改编与章节续写的骨架)。",
|
|
4516
|
+
"要求:",
|
|
4517
|
+
"1. 第一行写《书名》(可从正文推断,否则《未命名》)。",
|
|
4518
|
+
"2. 按故事弧线划分卷/部分:每卷给卷名与主旨(标注覆盖章节范围)。",
|
|
4519
|
+
"3. 对每章列出:章节号 + 标题 + 一句话核心情节。",
|
|
4520
|
+
"4. 最后给出:全书主线、主要人物弧线、已埋设待回收的伏笔清单。",
|
|
4521
|
+
"5. 输出纯文本 Markdown 结构(# 一级标题、## 二级标题、- 列表),不要寒暄,不要其他输出。"
|
|
4522
|
+
].join("\n"),
|
|
4523
|
+
user: sample.join("\n\n---\n\n"),
|
|
4524
|
+
temperature: .4,
|
|
4525
|
+
maxTokens: Math.max(config.maxTokens, 6e3),
|
|
4526
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
4527
|
+
})).trim();
|
|
4528
|
+
}
|
|
4529
|
+
/** 改编方案:由用户勾选的维度与新值,生成 LLM 映射表/规则/影响清单。 */
|
|
4530
|
+
async function proposeAdaptation(ctx, config, text, selections, dimensions) {
|
|
4531
|
+
const raw = parseJsonObject(await complete(ctx, config, {
|
|
4532
|
+
system: "你是改编策划。",
|
|
4533
|
+
user: [
|
|
4534
|
+
"下面给出改编决策:用户想改哪些维度、改成什么值。请生成一份可执行的「改编方案」。",
|
|
4535
|
+
"要求输出合法 JSON 对象:",
|
|
4536
|
+
"{\"mappings\": [{\"source\":\"原值\",\"target\":\"新值\",\"scope\":\"name|realm|faction|term|other\",\"note\":\"说明\"}], \"rules\": {\"preserve\":[\"必须保留的要素\"],\"change\":[\"允许改变的要素\"],\"constraints\":[\"改编红线/一致性要求\"]}, \"impacts\": [{\"item\":\"受影响项\",\"detail\":\"说明\",\"risk\":\"high|medium|low\",\"chapters\":[章号]}]}",
|
|
4537
|
+
"mappings 需把用户确认的新值展开为「原→新」条目(如主角名/境界名/势力名/术语),并补充用户未填但关联的必改项(如改了修为体系名,相关的境界名一并列映射)。",
|
|
4538
|
+
"rules.preserve 至少包含:故事骨架、人物动机、伏笔逻辑、爽点结构。",
|
|
4539
|
+
"impacts 列出每个改动会影响的内容(术语/角色/章节/伏笔),能定位章号尽量定位;无法定位则给章节区间提示。",
|
|
4540
|
+
"重要:所有字符串值内部不得包含换行符;JSON 必须在一段内完整结束;直接输出 JSON,不要 Markdown 代码块。",
|
|
4541
|
+
"",
|
|
4542
|
+
"用户要改:",
|
|
4543
|
+
selections.map((s) => "- " + s.title + "(" + s.key + "):" + s.current + " → " + s.target).join("\n"),
|
|
4544
|
+
"",
|
|
4545
|
+
"原文可改矩阵(已分析):",
|
|
4546
|
+
(dimensions ?? []).map((d) => "- " + d.title + ":" + d.current + "(可改度:" + d.mutability + ",风险:" + d.risk + ")").join("\n")
|
|
4547
|
+
].join("\n"),
|
|
4548
|
+
temperature: .3,
|
|
4549
|
+
maxTokens: Math.max(config.maxTokens, 6e3),
|
|
4550
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
4551
|
+
}));
|
|
4552
|
+
return { proposal: {
|
|
4553
|
+
mappings: Array.isArray(raw.mappings) ? raw.mappings.filter((m) => typeof m === "object" && m !== null).map((m) => normalizeAdaptationMapping(m)).filter((m) => m !== null) : [],
|
|
4554
|
+
rules: normalizeAdaptationRules(raw.rules),
|
|
4555
|
+
impacts: Array.isArray(raw.impacts) ? raw.impacts.filter((i) => typeof i === "object" && i !== null).map((i) => normalizeAdaptationImpact(i)) : []
|
|
4556
|
+
} };
|
|
4557
|
+
}
|
|
4558
|
+
/** 剧本术语替换执行:按映射表做精确替换并统计命中。 */
|
|
4559
|
+
function applyAdaptationReplacements(text, mappings) {
|
|
4560
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4561
|
+
const unique = mappings.filter((m) => {
|
|
4562
|
+
const s = m.source.trim();
|
|
4563
|
+
if (s === "" || s === m.target.trim()) return false;
|
|
4564
|
+
if (seen.has(s)) return false;
|
|
4565
|
+
seen.add(s);
|
|
4566
|
+
return true;
|
|
4567
|
+
}).sort((a, b) => b.source.length - a.source.length);
|
|
4568
|
+
let adapted = text;
|
|
4569
|
+
const hits = [];
|
|
4570
|
+
for (const m of unique) {
|
|
4571
|
+
const count = adapted.split(m.source).length - 1;
|
|
4572
|
+
if (count > 0) adapted = adapted.split(m.source).join(m.target);
|
|
4573
|
+
hits.push({
|
|
4574
|
+
source: m.source,
|
|
4575
|
+
target: m.target,
|
|
4576
|
+
count
|
|
4577
|
+
});
|
|
4578
|
+
}
|
|
4579
|
+
return {
|
|
4580
|
+
adaptedText: adapted,
|
|
4581
|
+
hits
|
|
4582
|
+
};
|
|
4583
|
+
}
|
|
4584
|
+
/** 校验归一化一条映射(来自 LLM)。 */
|
|
4585
|
+
function normalizeAdaptationMapping(m) {
|
|
4586
|
+
const source = typeof m.source === "string" ? m.source.trim() : "";
|
|
4587
|
+
const target = typeof m.target === "string" ? m.target.trim() : "";
|
|
4588
|
+
if (source === "" || target === "") return null;
|
|
4589
|
+
return {
|
|
4590
|
+
source,
|
|
4591
|
+
target,
|
|
4592
|
+
scope: [
|
|
4593
|
+
"name",
|
|
4594
|
+
"realm",
|
|
4595
|
+
"faction",
|
|
4596
|
+
"term",
|
|
4597
|
+
"other"
|
|
4598
|
+
].includes(m.scope) ? m.scope : "other",
|
|
4599
|
+
note: typeof m.note === "string" ? m.note : void 0
|
|
4600
|
+
};
|
|
4601
|
+
}
|
|
4602
|
+
/** 校验归一化改编规则(来自 LLM)。 */
|
|
4603
|
+
function normalizeAdaptationRules(r) {
|
|
4604
|
+
const obj = typeof r === "object" && r !== null ? r : {};
|
|
4605
|
+
const arr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
4606
|
+
return {
|
|
4607
|
+
preserve: arr(obj.preserve),
|
|
4608
|
+
change: arr(obj.change),
|
|
4609
|
+
constraints: arr(obj.constraints)
|
|
4610
|
+
};
|
|
4611
|
+
}
|
|
4612
|
+
/** 校验归一化一条影响项(来自 LLM)。 */
|
|
4613
|
+
function normalizeAdaptationImpact(i) {
|
|
4614
|
+
const risk = [
|
|
4615
|
+
"high",
|
|
4616
|
+
"medium",
|
|
4617
|
+
"low"
|
|
4618
|
+
].includes(i.risk) ? i.risk : "medium";
|
|
4619
|
+
const chapters = Array.isArray(i.chapters) ? i.chapters.filter((x) => typeof x === "number") : void 0;
|
|
4620
|
+
return {
|
|
4621
|
+
item: typeof i.item === "string" ? i.item : "",
|
|
4622
|
+
detail: typeof i.detail === "string" ? i.detail : "",
|
|
4623
|
+
risk,
|
|
4624
|
+
chapters: chapters !== void 0 && chapters.length > 0 ? chapters : void 0
|
|
4625
|
+
};
|
|
4626
|
+
}
|
|
4627
|
+
/**
|
|
4425
4628
|
* 摘要 + 事实抽取合并为一次 LLM 调用(省一次调用与一次正文输入,
|
|
4426
4629
|
* 批量生成时整体开销约省 25%)。
|
|
4427
4630
|
* @returns 摘要与新增事实条数(失败返回空,调用方 best-effort)。
|
|
@@ -5555,7 +5758,25 @@ const NOVEL_API = {
|
|
|
5555
5758
|
/** 生产单状态(含进度统计与日志)。 */
|
|
5556
5759
|
runStatus: "/api/dsh-novel-forge/run/status",
|
|
5557
5760
|
config: "/api/dsh-novel-forge/config",
|
|
5558
|
-
openFolder: "/api/dsh-novel-forge/open-folder"
|
|
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"
|
|
5559
5780
|
};
|
|
5560
5781
|
//#endregion
|
|
5561
5782
|
//#region src/docx.ts
|
|
@@ -6347,6 +6568,169 @@ async function* runAssistantTurn(ctx, config, project, outputDir, userMessage) {
|
|
|
6347
6568
|
}
|
|
6348
6569
|
}
|
|
6349
6570
|
//#endregion
|
|
6571
|
+
//#region src/author-assets.ts
|
|
6572
|
+
/**
|
|
6573
|
+
* 作者资产库/总数据(跨书)— 按作者维度聚合的可复用资产。
|
|
6574
|
+
* 与书架同惯例持久化到 ~/.dsh/dsh-novel-forge-author-assets.json。
|
|
6575
|
+
* 提供读取/持久化/新增或更新/删除,以及「导入默认」:把书架书的写作资产/角色 +
|
|
6576
|
+
* 内置全局库(题材/反AI规则/风格模板/推进模式)批量沉淀成资产条目(默认库)。
|
|
6577
|
+
*/
|
|
6578
|
+
/** 作者资产库配置文件路径。 */
|
|
6579
|
+
function authorAssetsFile() {
|
|
6580
|
+
return join(homedir(), ".dsh", "dsh-novel-forge-author-assets.json");
|
|
6581
|
+
}
|
|
6582
|
+
/** 默认空资产库。 */
|
|
6583
|
+
function defaultLibrary() {
|
|
6584
|
+
return {
|
|
6585
|
+
version: 1,
|
|
6586
|
+
items: []
|
|
6587
|
+
};
|
|
6588
|
+
}
|
|
6589
|
+
/** 读取作者资产库(不存在/损坏则返回空库,不抛错)。 */
|
|
6590
|
+
function loadAuthorAssets() {
|
|
6591
|
+
const file = authorAssetsFile();
|
|
6592
|
+
if (!existsSync(file)) return defaultLibrary();
|
|
6593
|
+
try {
|
|
6594
|
+
let raw = readFileSync(file, "utf8");
|
|
6595
|
+
if (raw.charCodeAt(0) === 65279) raw = raw.slice(1);
|
|
6596
|
+
const parsed = JSON.parse(raw);
|
|
6597
|
+
if (!Array.isArray(parsed.items)) return defaultLibrary();
|
|
6598
|
+
return {
|
|
6599
|
+
version: 1,
|
|
6600
|
+
items: parsed.items
|
|
6601
|
+
};
|
|
6602
|
+
} catch {
|
|
6603
|
+
return defaultLibrary();
|
|
6604
|
+
}
|
|
6605
|
+
}
|
|
6606
|
+
/** 持久化作者资产库。 */
|
|
6607
|
+
function saveAuthorAssets(library) {
|
|
6608
|
+
const file = authorAssetsFile();
|
|
6609
|
+
mkdirSync(join(homedir(), ".dsh"), { recursive: true });
|
|
6610
|
+
writeFileSync(file, JSON.stringify(library, null, 2), "utf8");
|
|
6611
|
+
}
|
|
6612
|
+
/** 按 id 更新一条资产;不存在则追加。 */
|
|
6613
|
+
function upsertAuthorAsset(input) {
|
|
6614
|
+
const library = loadAuthorAssets();
|
|
6615
|
+
const idx = library.items.findIndex((a) => a.id === input.id);
|
|
6616
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6617
|
+
if (idx >= 0) {
|
|
6618
|
+
const prev = library.items[idx];
|
|
6619
|
+
if (prev !== void 0) library.items[idx] = {
|
|
6620
|
+
...prev,
|
|
6621
|
+
...input,
|
|
6622
|
+
id: prev.id,
|
|
6623
|
+
createdAt: prev.createdAt,
|
|
6624
|
+
updatedAt: now
|
|
6625
|
+
};
|
|
6626
|
+
} else library.items.push({
|
|
6627
|
+
...input,
|
|
6628
|
+
createdAt: input.createdAt !== "" ? input.createdAt : now,
|
|
6629
|
+
updatedAt: now
|
|
6630
|
+
});
|
|
6631
|
+
saveAuthorAssets(library);
|
|
6632
|
+
return library;
|
|
6633
|
+
}
|
|
6634
|
+
/** 删除一条资产;返回更新后的资产库。 */
|
|
6635
|
+
function removeAuthorAsset(id) {
|
|
6636
|
+
const library = loadAuthorAssets();
|
|
6637
|
+
library.items = library.items.filter((a) => a.id !== id);
|
|
6638
|
+
saveAuthorAssets(library);
|
|
6639
|
+
return library;
|
|
6640
|
+
}
|
|
6641
|
+
/**
|
|
6642
|
+
* 导入默认:把书架所有书的写作资产/角色 + 内置全局库批量沉淀到作者资产库。
|
|
6643
|
+
* 按 `kind:name` 去重(已存在则跳过),不覆盖用户已有条目。
|
|
6644
|
+
* @returns 更新后的资产库。
|
|
6645
|
+
*/
|
|
6646
|
+
function importDefaultAuthorAssets() {
|
|
6647
|
+
const library = loadAuthorAssets();
|
|
6648
|
+
const seen = new Set(library.items.map((a) => a.kind + ":" + a.name));
|
|
6649
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6650
|
+
const push = (kind, name, summary, content, tags, sourceBooks, structured) => {
|
|
6651
|
+
const key = kind + ":" + name.trim();
|
|
6652
|
+
if (name.trim() === "" || seen.has(key)) return;
|
|
6653
|
+
seen.add(key);
|
|
6654
|
+
library.items.push({
|
|
6655
|
+
id: "aa-" + Date.now().toString(36) + "-" + randomBytes(3).toString("hex"),
|
|
6656
|
+
kind,
|
|
6657
|
+
name: name.trim(),
|
|
6658
|
+
summary: summary.trim(),
|
|
6659
|
+
content: content.trim(),
|
|
6660
|
+
sourceBooks,
|
|
6661
|
+
tags,
|
|
6662
|
+
structured,
|
|
6663
|
+
createdAt: now,
|
|
6664
|
+
updatedAt: now
|
|
6665
|
+
});
|
|
6666
|
+
};
|
|
6667
|
+
for (const g of flattenGenres(BUILTIN_GENRE_LIBRARY)) push("genre", g.name, g.description, g.description, ["内置", "题材"], [], {
|
|
6668
|
+
description: g.description,
|
|
6669
|
+
children: (g.children ?? []).map((c) => c.name)
|
|
6670
|
+
});
|
|
6671
|
+
for (const r of BUILTIN_ANTI_AI_RULES) push("antiAi", r.name, r.avoid, "避免:" + r.avoid + "\n修正:" + r.fix, ["内置", "反AI"], [], {
|
|
6672
|
+
avoid: r.avoid,
|
|
6673
|
+
fix: r.fix,
|
|
6674
|
+
detectPatterns: r.detectPatterns
|
|
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
|
+
});
|
|
6684
|
+
const shelf = loadBookshelf();
|
|
6685
|
+
for (const book of shelf.books) {
|
|
6686
|
+
const project = loadProject(book.outputDir);
|
|
6687
|
+
if (project === void 0) continue;
|
|
6688
|
+
const src = [project.bookName];
|
|
6689
|
+
const assets = project.assets;
|
|
6690
|
+
if (assets !== void 0) {
|
|
6691
|
+
if (assets.genre !== void 0) push("genre", assets.genre.name, assets.genre.description, assets.genre.description, ["书", "题材"], src, { description: assets.genre.description });
|
|
6692
|
+
if (assets.primaryProgression !== void 0) push("progression", assets.primaryProgression.name, assets.primaryProgression.driver, "驱动:" + assets.primaryProgression.driver + "\n期待:" + assets.primaryProgression.readerExpectation, src, [], {
|
|
6693
|
+
driver: assets.primaryProgression.driver,
|
|
6694
|
+
primary: true
|
|
6695
|
+
});
|
|
6696
|
+
for (const p of assets.auxiliaryProgressions ?? []) push("progression", p.name, p.driver, "驱动:" + p.driver + "\n期待:" + p.readerExpectation, src, [], {
|
|
6697
|
+
driver: p.driver,
|
|
6698
|
+
primary: false
|
|
6699
|
+
});
|
|
6700
|
+
for (const r of assets.antiAiRules ?? []) push("antiAi", r.name, r.avoid, "避免:" + r.avoid + "\n修正:" + r.fix, src, [], {
|
|
6701
|
+
avoid: r.avoid,
|
|
6702
|
+
fix: r.fix,
|
|
6703
|
+
detectPatterns: r.detectPatterns
|
|
6704
|
+
});
|
|
6705
|
+
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
|
+
}
|
|
6707
|
+
for (const role of project.roles ?? []) {
|
|
6708
|
+
const persona = ("身份:" + 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
|
+
push("roleTemplate", role.name, role.identity, persona, src, [], {
|
|
6710
|
+
roleLabel: role.roleLabel,
|
|
6711
|
+
traits: role.traits,
|
|
6712
|
+
relations: role.relations,
|
|
6713
|
+
arc: role.arc,
|
|
6714
|
+
knowledge: role.knowledge
|
|
6715
|
+
});
|
|
6716
|
+
}
|
|
6717
|
+
}
|
|
6718
|
+
saveAuthorAssets(library);
|
|
6719
|
+
return library;
|
|
6720
|
+
}
|
|
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
|
+
//#endregion
|
|
6350
6734
|
//#region src/routes.ts
|
|
6351
6735
|
/** Cap on JSON request bodies (generous: cover images travel as base64). */
|
|
6352
6736
|
const MAX_JSON_BODY_BYTES = 64 * 1024 * 1024;
|
|
@@ -8783,6 +9167,191 @@ function makeRoutes(deps) {
|
|
|
8783
9167
|
ctx,
|
|
8784
9168
|
getConfig
|
|
8785
9169
|
});
|
|
9170
|
+
const runStartRoute = {
|
|
9171
|
+
kind: "exact",
|
|
9172
|
+
path: NOVEL_API.runStart,
|
|
9173
|
+
handler: async (req, res) => {
|
|
9174
|
+
if (!guard(req, res, "POST")) return;
|
|
9175
|
+
const body = await readJsonBody(req);
|
|
9176
|
+
const project = loadProject(getConfig().outputDir);
|
|
9177
|
+
if (project === void 0) {
|
|
9178
|
+
writeJson(res, 400, { error: "输出目录中没有项目" });
|
|
9179
|
+
return;
|
|
9180
|
+
}
|
|
9181
|
+
const startNo = Number.isInteger(body?.startNo) && body.startNo >= 1 ? body.startNo : 1;
|
|
9182
|
+
let endNo;
|
|
9183
|
+
if (Number.isInteger(body?.endNo) && body.endNo >= startNo) endNo = body.endNo;
|
|
9184
|
+
else if (Number.isInteger(body?.count) && body.count >= 1 && body.count <= 200) endNo = Math.max(0, ...project.chapters.map((c) => c.no)) + body.count;
|
|
9185
|
+
else {
|
|
9186
|
+
writeJson(res, 400, { error: "请提供 endNo 或 count(1-200)" });
|
|
9187
|
+
return;
|
|
9188
|
+
}
|
|
9189
|
+
try {
|
|
9190
|
+
writeJson(res, 200, await runner.start(startNo, endNo));
|
|
9191
|
+
} catch (error) {
|
|
9192
|
+
writeJson(res, 409, { error: error.message });
|
|
9193
|
+
}
|
|
9194
|
+
}
|
|
9195
|
+
};
|
|
9196
|
+
const runControlRoute = {
|
|
9197
|
+
kind: "exact",
|
|
9198
|
+
path: NOVEL_API.runControl,
|
|
9199
|
+
handler: async (req, res) => {
|
|
9200
|
+
if (!guard(req, res, "POST")) return;
|
|
9201
|
+
const body = await readJsonBody(req);
|
|
9202
|
+
if (runner.status() === null) {
|
|
9203
|
+
writeJson(res, 400, { error: "没有生产单" });
|
|
9204
|
+
return;
|
|
9205
|
+
}
|
|
9206
|
+
if (body?.action === "pause") runner.pause();
|
|
9207
|
+
else if (body?.action === "resume") runner.resume();
|
|
9208
|
+
else if (body?.action === "stop") runner.stop();
|
|
9209
|
+
else {
|
|
9210
|
+
writeJson(res, 400, { error: "action 须为 pause / resume / stop" });
|
|
9211
|
+
return;
|
|
9212
|
+
}
|
|
9213
|
+
writeJson(res, 200, runner.status());
|
|
9214
|
+
}
|
|
9215
|
+
};
|
|
9216
|
+
const runStatusRoute = {
|
|
9217
|
+
kind: "exact",
|
|
9218
|
+
path: NOVEL_API.runStatus,
|
|
9219
|
+
handler: (req, res) => {
|
|
9220
|
+
if (!guard(req, res, "GET")) return;
|
|
9221
|
+
writeJson(res, 200, runner.status());
|
|
9222
|
+
}
|
|
9223
|
+
};
|
|
9224
|
+
const authorAssetsRoute = {
|
|
9225
|
+
kind: "exact",
|
|
9226
|
+
path: NOVEL_API.authorAssets,
|
|
9227
|
+
handler: (req, res) => {
|
|
9228
|
+
if (!guard(req, res, "GET")) return;
|
|
9229
|
+
writeJson(res, 200, { assets: loadAuthorAssets() });
|
|
9230
|
+
}
|
|
9231
|
+
};
|
|
9232
|
+
const authorAssetsUpsertRoute = {
|
|
9233
|
+
kind: "exact",
|
|
9234
|
+
path: NOVEL_API.authorAssetsUpsert,
|
|
9235
|
+
handler: async (req, res) => {
|
|
9236
|
+
if (!guard(req, res, "POST")) return;
|
|
9237
|
+
const asset = (await readJsonBody(req))?.asset;
|
|
9238
|
+
if (asset === void 0 || asset.name.trim() === "" || asset.content.trim() === "") {
|
|
9239
|
+
writeJson(res, 400, { error: "asset 须含 name / content" });
|
|
9240
|
+
return;
|
|
9241
|
+
}
|
|
9242
|
+
writeJson(res, 200, { assets: upsertAuthorAsset(asset) });
|
|
9243
|
+
}
|
|
9244
|
+
};
|
|
9245
|
+
const authorAssetsRemoveRoute = {
|
|
9246
|
+
kind: "exact",
|
|
9247
|
+
path: NOVEL_API.authorAssetsRemove,
|
|
9248
|
+
handler: async (req, res) => {
|
|
9249
|
+
if (!guard(req, res, "POST")) return;
|
|
9250
|
+
const id = (await readJsonBody(req))?.id ?? "";
|
|
9251
|
+
if (id === "") {
|
|
9252
|
+
writeJson(res, 400, { error: "id 必填" });
|
|
9253
|
+
return;
|
|
9254
|
+
}
|
|
9255
|
+
writeJson(res, 200, { assets: removeAuthorAsset(id) });
|
|
9256
|
+
}
|
|
9257
|
+
};
|
|
9258
|
+
const authorAssetsImportDefaultRoute = {
|
|
9259
|
+
kind: "exact",
|
|
9260
|
+
path: NOVEL_API.authorAssetsImportDefault,
|
|
9261
|
+
handler: (req, res) => {
|
|
9262
|
+
if (!guard(req, res, "POST")) return;
|
|
9263
|
+
writeJson(res, 200, { assets: importDefaultAuthorAssets() });
|
|
9264
|
+
}
|
|
9265
|
+
};
|
|
9266
|
+
const adaptAnalyzeRoute = {
|
|
9267
|
+
kind: "exact",
|
|
9268
|
+
path: NOVEL_API.adaptAnalyze,
|
|
9269
|
+
handler: async (req, res) => {
|
|
9270
|
+
if (!guard(req, res, "POST")) return;
|
|
9271
|
+
if (!getConfig().enableAdaptMode) {
|
|
9272
|
+
writeJson(res, 404, { error: "改编模式未开启" });
|
|
9273
|
+
return;
|
|
9274
|
+
}
|
|
9275
|
+
const body = await readJsonBody(req);
|
|
9276
|
+
const config = getConfig();
|
|
9277
|
+
let text = body?.text?.trim() ?? "";
|
|
9278
|
+
if (text === "" && body?.filePath !== void 0 && body.filePath !== "") try {
|
|
9279
|
+
text = readFileSync(body.filePath, "utf8");
|
|
9280
|
+
} catch (err) {
|
|
9281
|
+
writeJson(res, 400, { error: "读取文件失败:" + err.message });
|
|
9282
|
+
return;
|
|
9283
|
+
}
|
|
9284
|
+
if (text.length < 200) {
|
|
9285
|
+
writeJson(res, 400, { error: "全文内容过短(<200 字符),请上传完整小说文本" });
|
|
9286
|
+
return;
|
|
9287
|
+
}
|
|
9288
|
+
try {
|
|
9289
|
+
writeJson(res, 200, await analyzeAdaptation(ctx, config, text));
|
|
9290
|
+
} catch (err) {
|
|
9291
|
+
writeJson(res, 400, { error: err.message });
|
|
9292
|
+
}
|
|
9293
|
+
}
|
|
9294
|
+
};
|
|
9295
|
+
const adaptProposeRoute = {
|
|
9296
|
+
kind: "exact",
|
|
9297
|
+
path: NOVEL_API.adaptPropose,
|
|
9298
|
+
handler: async (req, res) => {
|
|
9299
|
+
if (!guard(req, res, "POST")) return;
|
|
9300
|
+
if (!getConfig().enableAdaptMode) {
|
|
9301
|
+
writeJson(res, 404, { error: "改编模式未开启" });
|
|
9302
|
+
return;
|
|
9303
|
+
}
|
|
9304
|
+
const body = await readJsonBody(req);
|
|
9305
|
+
const config = getConfig();
|
|
9306
|
+
const text = (body?.text ?? "").trim();
|
|
9307
|
+
const selections = body?.selections ?? [];
|
|
9308
|
+
if (text.length < 200 || selections.length === 0) {
|
|
9309
|
+
writeJson(res, 400, { error: "请提供全文与至少一条要改的维度" });
|
|
9310
|
+
return;
|
|
9311
|
+
}
|
|
9312
|
+
try {
|
|
9313
|
+
writeJson(res, 200, await proposeAdaptation(ctx, config, text, selections, body?.dimensions));
|
|
9314
|
+
} catch (err) {
|
|
9315
|
+
writeJson(res, 400, { error: err.message });
|
|
9316
|
+
}
|
|
9317
|
+
}
|
|
9318
|
+
};
|
|
9319
|
+
const adaptExecuteRoute = {
|
|
9320
|
+
kind: "exact",
|
|
9321
|
+
path: NOVEL_API.adaptExecute,
|
|
9322
|
+
handler: async (req, res) => {
|
|
9323
|
+
if (!guard(req, res, "POST")) return;
|
|
9324
|
+
if (!getConfig().enableAdaptMode) {
|
|
9325
|
+
writeJson(res, 404, { error: "改编模式未开启" });
|
|
9326
|
+
return;
|
|
9327
|
+
}
|
|
9328
|
+
const body = await readJsonBody(req);
|
|
9329
|
+
const text = body?.text ?? "";
|
|
9330
|
+
const mappings = body?.mappings ?? [];
|
|
9331
|
+
if (text.length < 200 || mappings.length === 0) {
|
|
9332
|
+
writeJson(res, 400, { error: "请提供全文与映射表" });
|
|
9333
|
+
return;
|
|
9334
|
+
}
|
|
9335
|
+
if (body?.mode === "rewrite") {
|
|
9336
|
+
writeJson(res, 400, { error: "rewrite 模式尚未实现,请使用 replace(术语替换)" });
|
|
9337
|
+
return;
|
|
9338
|
+
}
|
|
9339
|
+
const { adaptedText, hits } = applyAdaptationReplacements(text, mappings);
|
|
9340
|
+
writeJson(res, 200, {
|
|
9341
|
+
adaptedText,
|
|
9342
|
+
mappings: mappings.length,
|
|
9343
|
+
hits
|
|
9344
|
+
});
|
|
9345
|
+
}
|
|
9346
|
+
};
|
|
9347
|
+
const THEME_BG_DIR = join(homedir(), ".dsh", "dsh-novel-forge-assets");
|
|
9348
|
+
const MIME_BY_EXT = {
|
|
9349
|
+
png: "image/png",
|
|
9350
|
+
jpg: "image/jpeg",
|
|
9351
|
+
jpeg: "image/jpeg",
|
|
9352
|
+
webp: "image/webp",
|
|
9353
|
+
gif: "image/gif"
|
|
9354
|
+
};
|
|
8786
9355
|
return [
|
|
8787
9356
|
statusRoute,
|
|
8788
9357
|
loadOutlineRoute,
|
|
@@ -8842,58 +9411,65 @@ function makeRoutes(deps) {
|
|
|
8842
9411
|
storyboardTableRoute,
|
|
8843
9412
|
storyboardPromptsRoute,
|
|
8844
9413
|
breakdownRoute,
|
|
9414
|
+
runStartRoute,
|
|
9415
|
+
runControlRoute,
|
|
9416
|
+
runStatusRoute,
|
|
9417
|
+
authorAssetsRoute,
|
|
9418
|
+
authorAssetsUpsertRoute,
|
|
9419
|
+
authorAssetsRemoveRoute,
|
|
9420
|
+
authorAssetsImportDefaultRoute,
|
|
9421
|
+
adaptAnalyzeRoute,
|
|
9422
|
+
adaptProposeRoute,
|
|
9423
|
+
adaptExecuteRoute,
|
|
8845
9424
|
{
|
|
8846
9425
|
kind: "exact",
|
|
8847
|
-
path: NOVEL_API.
|
|
9426
|
+
path: NOVEL_API.themeBackgroundUpload,
|
|
8848
9427
|
handler: async (req, res) => {
|
|
8849
9428
|
if (!guard(req, res, "POST")) return;
|
|
8850
|
-
const
|
|
8851
|
-
const
|
|
8852
|
-
if (
|
|
8853
|
-
writeJson(res, 400, { error: "
|
|
9429
|
+
const dataUrl = (await readJsonBody(req))?.dataUrl ?? "";
|
|
9430
|
+
const m = /^data:image\/(png|jpeg|jpg|webp|gif);base64,(.+)$/i.exec(dataUrl);
|
|
9431
|
+
if (m === null) {
|
|
9432
|
+
writeJson(res, 400, { error: "仅支持 PNG/JPG/WebP/GIF 图片" });
|
|
8854
9433
|
return;
|
|
8855
9434
|
}
|
|
8856
|
-
const
|
|
8857
|
-
|
|
8858
|
-
if (
|
|
8859
|
-
|
|
8860
|
-
else {
|
|
8861
|
-
writeJson(res, 400, { error: "请提供 endNo 或 count(1-200)" });
|
|
9435
|
+
const ext = m[1].toLowerCase() === "jpg" ? "jpeg" : m[1].toLowerCase();
|
|
9436
|
+
const buf = Buffer.from(m[2], "base64");
|
|
9437
|
+
if (buf.length === 0) {
|
|
9438
|
+
writeJson(res, 400, { error: "图片数据为空" });
|
|
8862
9439
|
return;
|
|
8863
9440
|
}
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
}
|
|
9441
|
+
mkdirSync(THEME_BG_DIR, { recursive: true });
|
|
9442
|
+
const name = "theme-bg-" + Date.now().toString(36) + "-" + randomBytes(3).toString("hex") + "." + ext;
|
|
9443
|
+
writeFileSync(join(THEME_BG_DIR, name), buf);
|
|
9444
|
+
writeJson(res, 200, { url: NOVEL_API.themeBackgroundGet + "/" + name });
|
|
8869
9445
|
}
|
|
8870
9446
|
},
|
|
8871
9447
|
{
|
|
8872
|
-
kind: "
|
|
8873
|
-
path: NOVEL_API.
|
|
8874
|
-
handler:
|
|
8875
|
-
if (!guard(req, res, "
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
9448
|
+
kind: "prefix",
|
|
9449
|
+
path: NOVEL_API.themeBackgroundGet,
|
|
9450
|
+
handler: (req, res) => {
|
|
9451
|
+
if (!guard(req, res, "GET")) return;
|
|
9452
|
+
let pathName = "";
|
|
9453
|
+
try {
|
|
9454
|
+
pathName = new URL(req.url ?? "", "http://localhost").pathname;
|
|
9455
|
+
} catch {}
|
|
9456
|
+
const name = basename(pathName);
|
|
9457
|
+
if (!name.startsWith("theme-bg-")) {
|
|
9458
|
+
writeJson(res, 404, { error: "not found" });
|
|
8879
9459
|
return;
|
|
8880
9460
|
}
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
else {
|
|
8885
|
-
writeJson(res, 400, { error: "action 须为 pause / resume / stop" });
|
|
9461
|
+
const file = join(THEME_BG_DIR, name);
|
|
9462
|
+
if (!existsSync(file)) {
|
|
9463
|
+
writeJson(res, 404, { error: "not found" });
|
|
8886
9464
|
return;
|
|
8887
9465
|
}
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
if (!guard(req, res, "GET")) return;
|
|
8896
|
-
writeJson(res, 200, runner.status());
|
|
9466
|
+
const ext = extname(file).slice(1);
|
|
9467
|
+
const type = MIME_BY_EXT[ext] ?? "application/octet-stream";
|
|
9468
|
+
res.writeHead(200, {
|
|
9469
|
+
"content-type": type,
|
|
9470
|
+
"cache-control": "public, max-age=31536000"
|
|
9471
|
+
});
|
|
9472
|
+
res.end(readFileSync(file));
|
|
8897
9473
|
}
|
|
8898
9474
|
}
|
|
8899
9475
|
];
|
|
@@ -8957,7 +9533,11 @@ const Config = z.object({
|
|
|
8957
9533
|
})).default([]),
|
|
8958
9534
|
imageApiKey: z.string().default(""),
|
|
8959
9535
|
imageApiModel: z.string().default(""),
|
|
8960
|
-
imageApiEnabled: z.boolean().default(false)
|
|
9536
|
+
imageApiEnabled: z.boolean().default(false),
|
|
9537
|
+
themeBackground: z.string().default(""),
|
|
9538
|
+
themeBackgroundBlur: z.number().default(0),
|
|
9539
|
+
themeOpacity: z.number().default(100),
|
|
9540
|
+
enableAdaptMode: z.boolean().default(false)
|
|
8961
9541
|
});
|
|
8962
9542
|
/** Schema defaults, re-read for hand-built test contexts. */
|
|
8963
9543
|
const DEFAULT_ANNOUNCE = true;
|
|
@@ -9010,7 +9590,11 @@ function resolveConfig(value) {
|
|
|
9010
9590
|
imageApiModel: (() => {
|
|
9011
9591
|
return ((value?.imageModels ?? []).find((m) => m.enabled) ?? (value?.imageModels ?? [])[0])?.model ?? value?.imageApiModel;
|
|
9012
9592
|
})(),
|
|
9013
|
-
imageApiEnabled: value?.imageApiEnabled ?? false
|
|
9593
|
+
imageApiEnabled: value?.imageApiEnabled ?? false,
|
|
9594
|
+
themeBackground: value?.themeBackground ?? "",
|
|
9595
|
+
themeBackgroundBlur: value?.themeBackgroundBlur ?? 0,
|
|
9596
|
+
themeOpacity: value?.themeOpacity ?? 100,
|
|
9597
|
+
enableAdaptMode: value?.enableAdaptMode ?? false
|
|
9014
9598
|
};
|
|
9015
9599
|
}
|
|
9016
9600
|
/** 生图默认接口地址(豆包 ark,OpenAI 兼容)。 */
|
|
@@ -9048,6 +9632,10 @@ function apply(ctx, config) {
|
|
|
9048
9632
|
if (patch.imageApiKey !== void 0) next.imageApiKey = patch.imageApiKey;
|
|
9049
9633
|
if (patch.imageApiModel !== void 0) next.imageApiModel = patch.imageApiModel;
|
|
9050
9634
|
if (patch.imageApiEnabled !== void 0) next.imageApiEnabled = patch.imageApiEnabled;
|
|
9635
|
+
if (patch.themeBackground !== void 0) next.themeBackground = patch.themeBackground;
|
|
9636
|
+
if (patch.themeBackgroundBlur !== void 0) next.themeBackgroundBlur = patch.themeBackgroundBlur;
|
|
9637
|
+
if (patch.themeOpacity !== void 0) next.themeOpacity = patch.themeOpacity;
|
|
9638
|
+
if (patch.enableAdaptMode !== void 0) next.enableAdaptMode = patch.enableAdaptMode;
|
|
9051
9639
|
if (patch.imageModels !== void 0) next.imageModels = patch.imageModels;
|
|
9052
9640
|
const settings = ctx.get("settings");
|
|
9053
9641
|
if (settings !== void 0) await settings.update(NOVEL_SETTINGS_NAMESPACE, next);
|