@waterwx/dsh-novel-forge 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +825 -564
- package/lib/client.js.map +1 -1
- package/lib/index.js +330 -58
- package/lib/index.js.map +1 -1
- package/lib/types/client/api.d.ts +7 -1
- package/lib/types/client/locales.d.ts +4 -2
- package/lib/types/engine.d.ts +27 -1
- package/lib/types/index.d.ts +2 -0
- package/lib/types/protocol.d.ts +45 -3
- package/package.json +1 -1
- package/src/client/api.ts +16 -1
- package/src/client/locales.ts +7 -3
- package/src/client/panel/ImportModal.tsx +123 -21
- package/src/client/panel/NovelPanel.tsx +221 -147
- package/src/client/panel/panel.module.css +77 -0
- package/src/engine.ts +210 -39
- package/src/index.ts +6 -0
- package/src/protocol.ts +36 -3
- package/src/routes.ts +88 -11
package/lib/index.js
CHANGED
|
@@ -690,6 +690,25 @@ const COMPLIANCE_REDLINES = [
|
|
|
690
690
|
];
|
|
691
691
|
/** Project state file name inside the output dir. */
|
|
692
692
|
const PROJECT_FILE = "novel-project.json";
|
|
693
|
+
/** 智能解码文本文件:UTF-8 BOM / UTF-16 BOM / UTF-8(严格校验)/ GB18030 回退。 */
|
|
694
|
+
function decodeTextSmart(buf) {
|
|
695
|
+
if (buf.length >= 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191) return buf.subarray(3).toString("utf8");
|
|
696
|
+
if (buf.length >= 2 && buf[0] === 255 && buf[1] === 254) return buf.subarray(2).toString("utf16le");
|
|
697
|
+
const utf8 = buf.toString("utf8");
|
|
698
|
+
const bad = countReplacementChars(utf8);
|
|
699
|
+
if (bad === 0) return utf8;
|
|
700
|
+
try {
|
|
701
|
+
const gbk = new TextDecoder("gb18030").decode(buf);
|
|
702
|
+
if (countReplacementChars(gbk) < bad) return gbk;
|
|
703
|
+
} catch {}
|
|
704
|
+
return utf8;
|
|
705
|
+
}
|
|
706
|
+
/** 统计替换字符 U+FFFD 数量(UTF-8 乱码检测)。 */
|
|
707
|
+
function countReplacementChars(s) {
|
|
708
|
+
let n = 0;
|
|
709
|
+
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 65533) n++;
|
|
710
|
+
return n;
|
|
711
|
+
}
|
|
693
712
|
/** Sanitize a file name: keep CJK/alphanumerics/space/dash/underscore. */
|
|
694
713
|
function safeFileName(name) {
|
|
695
714
|
return name.replace(/[\\/:*?"<>|]/g, "").replace(/\s+/g, " ").trim().slice(0, 60);
|
|
@@ -1167,7 +1186,7 @@ async function complete(ctx, config, options) {
|
|
|
1167
1186
|
system: options.system,
|
|
1168
1187
|
maxTokens: options.maxTokens ?? config.maxTokens,
|
|
1169
1188
|
temperature: options.temperature ?? .7,
|
|
1170
|
-
reasoningEffort: ReasoningEffortId(config.reasoningEffort ?? "off")
|
|
1189
|
+
reasoningEffort: ReasoningEffortId(options.reasoning ?? config.reasoningEffort ?? "off")
|
|
1171
1190
|
};
|
|
1172
1191
|
const assembler = new BlockAssembler();
|
|
1173
1192
|
for await (const chunk of ctx.llm.stream(request)) assembler.push(chunk);
|
|
@@ -1293,7 +1312,8 @@ async function extractBible(ctx, config, outline, project) {
|
|
|
1293
1312
|
system: bibleSystemPrompt(),
|
|
1294
1313
|
user,
|
|
1295
1314
|
temperature: .4,
|
|
1296
|
-
maxTokens: Math.max(config.maxTokens, 16e3)
|
|
1315
|
+
maxTokens: Math.max(config.maxTokens, 16e3),
|
|
1316
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
1297
1317
|
}));
|
|
1298
1318
|
const strArray = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string" && v.trim() !== "") : [];
|
|
1299
1319
|
const characters = Array.isArray(raw.characters) ? raw.characters.filter((v) => typeof v === "object" && v !== null).map((entry) => ({
|
|
@@ -1756,7 +1776,8 @@ async function suggestPlotlines(ctx, config, project) {
|
|
|
1756
1776
|
"只输出 JSON 数组。"
|
|
1757
1777
|
].join("\n\n"),
|
|
1758
1778
|
temperature: .6,
|
|
1759
|
-
maxTokens: Math.max(config.maxTokens, 4e3)
|
|
1779
|
+
maxTokens: Math.max(config.maxTokens, 4e3),
|
|
1780
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
1760
1781
|
}));
|
|
1761
1782
|
const lines = [];
|
|
1762
1783
|
const kinds = /* @__PURE__ */ new Set([
|
|
@@ -1803,7 +1824,8 @@ async function refreshPlotlineProgress(ctx, config, project, line) {
|
|
|
1803
1824
|
"只输出 JSON 对象。"
|
|
1804
1825
|
].join("\n\n"),
|
|
1805
1826
|
temperature: .3,
|
|
1806
|
-
maxTokens: Math.max(config.maxTokens, 2e3)
|
|
1827
|
+
maxTokens: Math.max(config.maxTokens, 2e3),
|
|
1828
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
1807
1829
|
}));
|
|
1808
1830
|
return typeof raw.progress === "string" ? raw.progress.trim().slice(0, 300) : "";
|
|
1809
1831
|
}
|
|
@@ -1813,6 +1835,8 @@ async function extractRoles(ctx, config, project) {
|
|
|
1813
1835
|
"你是一位网文角色库管理员。请根据本书的大纲、设定、编年录与章节摘要,提炼完整的角色库。",
|
|
1814
1836
|
"覆盖原则:所有在编年录/章节中实际出场或有名有姓的角色都应收录;无名的功能性人物(如\"矮胖姑娘\")用其身份简称收录并标注。",
|
|
1815
1837
|
"数量控制:最多输出 10 个角色,宁缺毋滥;路人级一次带过的不要收录。",
|
|
1838
|
+
"重要:正文中若有明确的主角与主要反派,必须分别以 protagonist / antagonist 收录,禁止遗漏;反派确实未出场时才可省略。",
|
|
1839
|
+
"输出优先级:主角(protagonist)与主要反派(antagonist)必须优先输出并完整刻画,其次女主/重要配角;判断不出名字时用正文中的身份称呼。",
|
|
1816
1840
|
"每个角色输出:",
|
|
1817
1841
|
"1. name:角色名(或身份简称)。主角/有名配角必须用正文中实际出现的人名(如「沈放」),禁止用「主角(38岁超市理货员)」这类把身份塞进名字的占位名;正文确实没点名时才可用身份简称(如「富商」「灰衣老人」)。",
|
|
1818
1842
|
"2. roleLabel:定位——protagonist=主角;female_lead=女主(唯一知己/感情线核心,无后宫前提下只此一位);female_support=重要女配;support=普通配角;antagonist=反派;extra=路人/背景。",
|
|
@@ -1830,28 +1854,50 @@ async function extractRoles(ctx, config, project) {
|
|
|
1830
1854
|
].join("\n");
|
|
1831
1855
|
const written = project.chapters.filter((c) => c.status !== "pending" && c.status !== "generating");
|
|
1832
1856
|
const existingRoles = project.roles ?? [];
|
|
1857
|
+
const sampleChapters = written.length <= 6 ? written : (() => {
|
|
1858
|
+
const picked = /* @__PURE__ */ new Set();
|
|
1859
|
+
for (let i = 0; i < 3 && i < written.length; i++) picked.add(i);
|
|
1860
|
+
const step = Math.max(1, Math.floor(written.length / 4));
|
|
1861
|
+
for (let i = step; i < written.length - 2; i += step) picked.add(i);
|
|
1862
|
+
for (let i = Math.max(0, written.length - 2); i < written.length; i++) picked.add(i);
|
|
1863
|
+
return [...picked].sort((a, b) => a - b).map((i) => written[i]);
|
|
1864
|
+
})();
|
|
1833
1865
|
const excerptParts = [];
|
|
1834
|
-
for (const chapter of
|
|
1866
|
+
for (const chapter of sampleChapters) {
|
|
1835
1867
|
const body = readChapterFile(config.outputDir, chapter);
|
|
1836
1868
|
if (body === void 0) continue;
|
|
1837
1869
|
const text = body.replace(/^#.*$/gm, "").trim();
|
|
1838
|
-
if (text.length > 0) excerptParts.push(`第${chapter.no}章《${chapter.title}》\n${text.slice(0,
|
|
1870
|
+
if (text.length > 0) excerptParts.push(`第${chapter.no}章《${chapter.title}》\n${text.slice(0, 1500)}`);
|
|
1839
1871
|
}
|
|
1840
|
-
const
|
|
1872
|
+
const user = [
|
|
1873
|
+
`书名:《${project.bookName}》`,
|
|
1874
|
+
existingRoles.length > 0 ? `已收录角色(跳过,不要输出):${existingRoles.map((r) => r.name).join("、")}` : "",
|
|
1875
|
+
`大纲(节选前 3000 字):\n${project.outline.slice(0, 3e3)}`,
|
|
1876
|
+
excerptParts.length > 0 ? `已写正文摘录(角色姓名/身份/关系以正文为准):\n${excerptParts.join("\n\n")}` : "",
|
|
1877
|
+
project.bible !== void 0 && project.bible.characters.length > 0 ? `已有角色卡(补充信息):\n${project.bible.characters.map((c) => `- ${c.name}(${c.role}):${c.traits.join("、")}${c.goals !== "" ? `;目标:${c.goals}` : ""}`).join("\n")}` : "",
|
|
1878
|
+
(project.facts ?? []).length > 0 ? `编年录(最近 30 条):\n${(project.facts ?? []).slice(-30).map((f) => `[第${f.chapterNo}章] ${f.text.slice(0, 60)}`).join("\n")}` : "",
|
|
1879
|
+
written.length > 0 ? `已写章节标题(${written.length} 章):\n${written.map((c) => `第${c.no}章《${c.title}》`).join("、")}` : "",
|
|
1880
|
+
"只输出 JSON 数组。"
|
|
1881
|
+
].join("\n\n");
|
|
1882
|
+
let text = await complete(ctx, config, {
|
|
1841
1883
|
system,
|
|
1842
|
-
user
|
|
1843
|
-
`书名:《${project.bookName}》`,
|
|
1844
|
-
existingRoles.length > 0 ? `已收录角色(跳过,不要输出):${existingRoles.map((r) => r.name).join("、")}` : "",
|
|
1845
|
-
`大纲(节选前 3000 字):\n${project.outline.slice(0, 3e3)}`,
|
|
1846
|
-
excerptParts.length > 0 ? `已写正文摘录(角色姓名/身份/关系以正文为准):\n${excerptParts.join("\n\n")}` : "",
|
|
1847
|
-
project.bible !== void 0 && project.bible.characters.length > 0 ? `已有角色卡(补充信息):\n${project.bible.characters.map((c) => `- ${c.name}(${c.role}):${c.traits.join("、")}${c.goals !== "" ? `;目标:${c.goals}` : ""}`).join("\n")}` : "",
|
|
1848
|
-
(project.facts ?? []).length > 0 ? `编年录(最近 60 条):\n${(project.facts ?? []).slice(-60).map((f) => `[第${f.chapterNo}章] ${f.text.slice(0, 80)}`).join("\n")}` : "",
|
|
1849
|
-
written.length > 0 ? `已写章节标题(${written.length} 章):\n${written.map((c) => `第${c.no}章《${c.title}》`).join("、")}` : "",
|
|
1850
|
-
"只输出 JSON 数组。"
|
|
1851
|
-
].join("\n\n"),
|
|
1884
|
+
user,
|
|
1852
1885
|
temperature: .3,
|
|
1853
|
-
maxTokens: Math.max(config.maxTokens, 24e3)
|
|
1854
|
-
|
|
1886
|
+
maxTokens: Math.max(config.maxTokens, 24e3),
|
|
1887
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
1888
|
+
});
|
|
1889
|
+
let raw = parseJsonArray(text);
|
|
1890
|
+
const hasProtagonist = raw.some((e) => typeof e === "object" && e !== null && e.roleLabel === "protagonist");
|
|
1891
|
+
if (raw.length === 0 || !hasProtagonist) {
|
|
1892
|
+
text = await complete(ctx, config, {
|
|
1893
|
+
system: system + (raw.length === 0 ? "\n上一次输出为空或格式不正确。请直接输出 JSON 数组(即使只有一个角色也要输出),不要输出其他文字。" : "\n上一次输出中缺少主角(roleLabel 为 protagonist 的角色)。请重新输出完整 JSON 数组,务必包含正文中的主角。"),
|
|
1894
|
+
user,
|
|
1895
|
+
temperature: .3,
|
|
1896
|
+
maxTokens: Math.max(config.maxTokens, 24e3),
|
|
1897
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
1898
|
+
});
|
|
1899
|
+
raw = parseJsonArray(text);
|
|
1900
|
+
}
|
|
1855
1901
|
const labels = /* @__PURE__ */ new Set([
|
|
1856
1902
|
"protagonist",
|
|
1857
1903
|
"female_lead",
|
|
@@ -1921,7 +1967,8 @@ async function extractScenes(ctx, config, project) {
|
|
|
1921
1967
|
"只输出 JSON 数组。"
|
|
1922
1968
|
].filter((s) => s !== "").join("\n"),
|
|
1923
1969
|
temperature: .4,
|
|
1924
|
-
maxTokens: Math.max(config.maxTokens, 12e3)
|
|
1970
|
+
maxTokens: Math.max(config.maxTokens, 12e3),
|
|
1971
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
1925
1972
|
}));
|
|
1926
1973
|
const strArr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.trim() !== "") : [];
|
|
1927
1974
|
const str = (v) => typeof v === "string" ? v.trim() : "";
|
|
@@ -1948,10 +1995,13 @@ async function extractScenes(ctx, config, project) {
|
|
|
1948
1995
|
}
|
|
1949
1996
|
return scenes;
|
|
1950
1997
|
}
|
|
1951
|
-
/** 从 txt/md
|
|
1952
|
-
function
|
|
1953
|
-
const lines =
|
|
1998
|
+
/** 从 txt/md 全本文本拆章(纯逻辑,不落盘):识别章节头、剥离重复标题、去重、排序并统一重新编号。 */
|
|
1999
|
+
function splitBookText(raw) {
|
|
2000
|
+
const lines = raw.split(/\r?\n/);
|
|
1954
2001
|
const chapterHead = /^\s*(?:#\s*)?第\s*(\d+|[一二三四五六七八九十百千]+)\s*[章回节卷]\s*(.*?)\s*$/;
|
|
2002
|
+
const cnOnlyHead = /^\s*(?:#\s*)?([一二三四五六七八九十百千万零〇]{1,6})(?:[、..::]\s*(.*?)\s*)?$/;
|
|
2003
|
+
const specialHead = /^\s*(?:#\s*)?(序章|序言|楔子|引子|前言|开篇|尾声|终章|大结局|番外(?:篇|章)?|后记|完结感言|上架感言|作者的话)\s*[::、.\s]*(.*?)\s*$/;
|
|
2004
|
+
const enHead = /^\s*(?:#\s*)?chapter\s+(\d+)\s*[.::、\-\s]*(.*?)\s*$/i;
|
|
1955
2005
|
const cnNum = {
|
|
1956
2006
|
一: 1,
|
|
1957
2007
|
二: 2,
|
|
@@ -1982,33 +2032,107 @@ function importBookText(filePath, outputDir) {
|
|
|
1982
2032
|
};
|
|
1983
2033
|
const chunks = [];
|
|
1984
2034
|
let current = null;
|
|
2035
|
+
let specialSeq = 0;
|
|
1985
2036
|
for (const line of lines) {
|
|
1986
2037
|
const m = chapterHead.exec(line);
|
|
1987
2038
|
if (m !== null) {
|
|
1988
|
-
if (current !== null) chunks.push(current);
|
|
1989
2039
|
const no = parseCn(m[1]);
|
|
1990
|
-
|
|
2040
|
+
if (current !== null) chunks.push(current);
|
|
2041
|
+
current = {
|
|
2042
|
+
sortKey: no > 0 ? no : chunks.length + 1,
|
|
2043
|
+
title: (m[2] ?? "").trim(),
|
|
2044
|
+
body: []
|
|
2045
|
+
};
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
const mc = cnOnlyHead.exec(line);
|
|
2049
|
+
if (mc !== null) {
|
|
2050
|
+
const no = parseCn(mc[1]);
|
|
2051
|
+
if (current !== null) chunks.push(current);
|
|
2052
|
+
const t = (mc[2] ?? "").trim();
|
|
2053
|
+
current = {
|
|
2054
|
+
sortKey: no > 0 ? no : chunks.length + 1,
|
|
2055
|
+
title: t !== "" ? t : "第" + mc[1] + "章",
|
|
2056
|
+
body: []
|
|
2057
|
+
};
|
|
2058
|
+
continue;
|
|
2059
|
+
}
|
|
2060
|
+
const ms = specialHead.exec(line);
|
|
2061
|
+
if (ms !== null) {
|
|
2062
|
+
specialSeq++;
|
|
2063
|
+
const name = ms[1];
|
|
2064
|
+
const front = /序章|序言|楔子|引子|前言|开篇/.test(name);
|
|
2065
|
+
const title = (ms[2] ?? "").trim();
|
|
2066
|
+
if (current !== null) chunks.push(current);
|
|
2067
|
+
current = {
|
|
2068
|
+
sortKey: front ? specialSeq * .001 : 999999 + specialSeq * .001,
|
|
2069
|
+
title: title !== "" ? title : name,
|
|
2070
|
+
body: []
|
|
2071
|
+
};
|
|
2072
|
+
continue;
|
|
2073
|
+
}
|
|
2074
|
+
const me = enHead.exec(line);
|
|
2075
|
+
if (me !== null) {
|
|
2076
|
+
if (current !== null) chunks.push(current);
|
|
1991
2077
|
current = {
|
|
1992
|
-
|
|
1993
|
-
title,
|
|
2078
|
+
sortKey: Number(me[1]),
|
|
2079
|
+
title: (me[2] ?? "").trim(),
|
|
1994
2080
|
body: []
|
|
1995
2081
|
};
|
|
1996
|
-
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
if (current !== null) current.body.push(line);
|
|
1997
2085
|
}
|
|
1998
2086
|
if (current !== null) chunks.push(current);
|
|
1999
|
-
if (chunks.length === 0) throw new Error("未识别到章节(需要\"第X章\"
|
|
2000
|
-
const
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2087
|
+
if (chunks.length === 0) throw new Error("未识别到章节(需要\"第X章\"格式、中文数字章节(一、二、三…)、序章/楔子/尾声等章节标题、英文 Chapter N,或带 # 的章节标题)");
|
|
2088
|
+
const stripHead = (b) => {
|
|
2089
|
+
let i = 0;
|
|
2090
|
+
while (i < b.length) {
|
|
2091
|
+
const t = b[i].trim();
|
|
2092
|
+
if (t === "" || chapterHead.test(t) || cnOnlyHead.test(t) || specialHead.test(t) || enHead.test(t)) i++;
|
|
2093
|
+
else break;
|
|
2094
|
+
}
|
|
2095
|
+
return b.slice(i).join("\n").trim();
|
|
2096
|
+
};
|
|
2097
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2098
|
+
for (const c of chunks) {
|
|
2099
|
+
const len = stripHead(c.body).length;
|
|
2100
|
+
const ex = byKey.get(c.sortKey);
|
|
2101
|
+
if (ex === void 0 || len > stripHead(ex.body).length) byKey.set(c.sortKey, c);
|
|
2102
|
+
}
|
|
2103
|
+
return [...byKey.values()].sort((a, b) => a.sortKey - b.sortKey).map((c, i) => ({
|
|
2104
|
+
no: i + 1,
|
|
2105
|
+
title: c.title,
|
|
2106
|
+
body: stripHead(c.body)
|
|
2107
|
+
}));
|
|
2108
|
+
}
|
|
2109
|
+
/** 拆章预览(不落盘):章节编号/标题/字数 + 跳过清单。 */
|
|
2110
|
+
function previewBookText(raw) {
|
|
2111
|
+
const chapters = [];
|
|
2112
|
+
const skipped = [];
|
|
2113
|
+
for (const c of splitBookText(raw)) {
|
|
2114
|
+
if (c.body.length < 50) {
|
|
2115
|
+
skipped.push("第" + c.no + "章" + (c.title !== "" ? "「" + c.title + "」" : "") + "(内容过短,已跳过)");
|
|
2116
|
+
continue;
|
|
2117
|
+
}
|
|
2118
|
+
chapters.push({
|
|
2119
|
+
no: c.no,
|
|
2120
|
+
title: c.title !== "" ? c.title : "第" + c.no + "章",
|
|
2121
|
+
chars: c.body.length
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
return {
|
|
2125
|
+
chapters,
|
|
2126
|
+
skipped
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
/** 从全本文本导入(浏览器上传 / 服务器文件共用):建项目、写章节文件、保存。 */
|
|
2130
|
+
function importBookTextFromText(raw, outputDir, bookName) {
|
|
2007
2131
|
const project = createProject(bookName);
|
|
2008
2132
|
mkdirSync(outputDir, { recursive: true });
|
|
2009
2133
|
const skipped = [];
|
|
2010
|
-
for (const c of
|
|
2011
|
-
const body = c.body
|
|
2134
|
+
for (const c of splitBookText(raw)) {
|
|
2135
|
+
const body = c.body;
|
|
2012
2136
|
if (body.length < 50) {
|
|
2013
2137
|
skipped.push("第" + c.no + "章" + (c.title !== "" ? "「" + c.title + "」" : "") + "(内容过短,已跳过)");
|
|
2014
2138
|
continue;
|
|
@@ -2036,6 +2160,10 @@ function importBookText(filePath, outputDir) {
|
|
|
2036
2160
|
skipped
|
|
2037
2161
|
};
|
|
2038
2162
|
}
|
|
2163
|
+
/** 从 txt/md 全本文件导入:编码自适应读取后拆章建项目,status=written(待审稿)。 */
|
|
2164
|
+
function importBookText(filePath, outputDir) {
|
|
2165
|
+
return importBookTextFromText(decodeTextSmart(readFileSync(filePath)), outputDir, basename(filePath, extname(filePath)).slice(0, 40) || "导入小说");
|
|
2166
|
+
}
|
|
2039
2167
|
/**
|
|
2040
2168
|
* 为单个角色提炼「动漫形象描述词」:扫描该角色出场的已写章节正文,
|
|
2041
2169
|
* 截取含外貌描写的段落,交给 LLM 提炼中文描述 + 英文绘图标签。
|
|
@@ -2166,7 +2294,8 @@ async function extractRoleVisual(ctx, config, project, outputDir, roleName) {
|
|
|
2166
2294
|
"只输出 JSON 对象。"
|
|
2167
2295
|
].join("\n\n"),
|
|
2168
2296
|
temperature: .4,
|
|
2169
|
-
maxTokens: Math.max(config.maxTokens, 12e3)
|
|
2297
|
+
maxTokens: Math.max(config.maxTokens, 12e3),
|
|
2298
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
2170
2299
|
}));
|
|
2171
2300
|
const zh = typeof raw.zh === "string" ? raw.zh.trim().slice(0, 500) : "";
|
|
2172
2301
|
const en = typeof raw.en === "string" ? raw.en.trim().slice(0, 1500) : "";
|
|
@@ -2239,7 +2368,8 @@ async function generateRolePromptKit(ctx, config, project, roleName) {
|
|
|
2239
2368
|
"只输出 JSON 对象。"
|
|
2240
2369
|
].join("\n\n"),
|
|
2241
2370
|
temperature: .4,
|
|
2242
|
-
maxTokens: Math.max(config.maxTokens, 12e3)
|
|
2371
|
+
maxTokens: Math.max(config.maxTokens, 12e3),
|
|
2372
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
2243
2373
|
}));
|
|
2244
2374
|
const str = (v) => typeof v === "string" ? v.trim() : "";
|
|
2245
2375
|
const pair = (v) => {
|
|
@@ -2284,7 +2414,8 @@ async function extractVisualRules(ctx, config, project) {
|
|
|
2284
2414
|
"只输出 JSON 数组。"
|
|
2285
2415
|
].filter((s) => s !== "").join("\n\n"),
|
|
2286
2416
|
temperature: .3,
|
|
2287
|
-
maxTokens: Math.max(config.maxTokens, 4e3)
|
|
2417
|
+
maxTokens: Math.max(config.maxTokens, 4e3),
|
|
2418
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
2288
2419
|
})).map((r) => typeof r === "string" ? r.trim().slice(0, 80) : "").filter((r) => r !== "").slice(0, 8);
|
|
2289
2420
|
}
|
|
2290
2421
|
/**
|
|
@@ -2320,7 +2451,8 @@ async function suggestOutlines(ctx, config, idea, count = 3, exclude = []) {
|
|
|
2320
2451
|
"只输出 JSON 数组。"
|
|
2321
2452
|
].join("\n\n"),
|
|
2322
2453
|
temperature: .85,
|
|
2323
|
-
maxTokens: Math.max(config.maxTokens, 12e3)
|
|
2454
|
+
maxTokens: Math.max(config.maxTokens, 12e3),
|
|
2455
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
2324
2456
|
}));
|
|
2325
2457
|
const candidates = [];
|
|
2326
2458
|
for (const entry of parsed) {
|
|
@@ -2390,7 +2522,8 @@ async function breakdownBook(ctx, config, project, outputDir, scope = "recent",
|
|
|
2390
2522
|
system: noteSystem,
|
|
2391
2523
|
user: noteUser,
|
|
2392
2524
|
temperature: .2,
|
|
2393
|
-
maxTokens: Math.max(config.maxTokens, 3e3)
|
|
2525
|
+
maxTokens: Math.max(config.maxTokens, 3e3),
|
|
2526
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
2394
2527
|
}));
|
|
2395
2528
|
const pick = (k) => Array.isArray(raw[k]) ? raw[k].filter((x) => typeof x === "string" && x.trim() !== "").map((x) => x.trim().slice(0, 120)).slice(0, 4) : [];
|
|
2396
2529
|
notes.push(`【第${ch.no}章《${ch.title}》】\n摘要:${typeof raw.summary === "string" ? raw.summary.slice(0, 200) : ""}\n剧情:${pick("plotPoints").join(";")}\n人物:${pick("characters").join(";")}\n设定:${pick("worldbuilding").join(";")}\n写法:${pick("styleTechniques").join(";")}\n卖点:${pick("marketHighlights").join(";")}\n短板信号:${pick("weaknessSignals").join(";") || "(无明显短板信号)"}`);
|
|
@@ -2462,7 +2595,8 @@ async function breakdownBook(ctx, config, project, outputDir, scope = "recent",
|
|
|
2462
2595
|
system: sec.system,
|
|
2463
2596
|
user: `分析范围:${selected.length} 章(${scope === "all" ? "全书" : scope === "recent" ? "最近 20 章" : "指定卷"})。\n\n章节笔记:\n${notesText}`,
|
|
2464
2597
|
temperature: .3,
|
|
2465
|
-
maxTokens: Math.max(config.maxTokens, 6e3)
|
|
2598
|
+
maxTokens: Math.max(config.maxTokens, 6e3),
|
|
2599
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
2466
2600
|
}));
|
|
2467
2601
|
sections.push({
|
|
2468
2602
|
key: sec.key,
|
|
@@ -2916,6 +3050,48 @@ async function summarizeChapter(ctx, config, project, outputDir, chapterNo) {
|
|
|
2916
3050
|
return chapter.summary;
|
|
2917
3051
|
}
|
|
2918
3052
|
/**
|
|
3053
|
+
* 反向推大纲:从已写章节正文反推出全书总纲(分卷 + 章节要点 + 主线/人物弧线/伏笔清单)。
|
|
3054
|
+
* 两阶段:分批提取章节事件摘要 → 汇总生成大纲。不修改章节/设定,只返回大纲文本。
|
|
3055
|
+
*/
|
|
3056
|
+
async function reverseOutlineFromChapters(ctx, config, project, outputDir, onProgress) {
|
|
3057
|
+
const written = project.chapters.filter((c) => c.status !== "pending" && c.status !== "generating" && c.status !== "error").filter((c) => readChapterFile(outputDir, c) !== void 0).sort((a, b) => a.no - b.no);
|
|
3058
|
+
if (written.length === 0) throw new Error("本书还没有已写章节,无法反推大纲");
|
|
3059
|
+
const BATCH = 10;
|
|
3060
|
+
const notes = [];
|
|
3061
|
+
const total = written.length;
|
|
3062
|
+
for (let i = 0; i < written.length; i += BATCH) {
|
|
3063
|
+
const note = await complete(ctx, config, {
|
|
3064
|
+
system: "你是一位网文编辑。下面是一本书若干章正文的节选。请为每一章输出一行「事件摘要」,格式严格为:第N章《标题》:关键事件+主角状态变化+新增伏笔或线索。每章恰好一行,不要空行,不要评价,不要输出其他内容。",
|
|
3065
|
+
user: written.slice(i, i + BATCH).map((c) => {
|
|
3066
|
+
const body = readChapterFile(outputDir, c) ?? "";
|
|
3067
|
+
return "第" + c.no + "章《" + (c.title || "无题") + "》\n" + body.replace(/^#\s+.*$/m, "").trim().slice(0, 1e3);
|
|
3068
|
+
}).join("\n\n---\n\n"),
|
|
3069
|
+
temperature: .2,
|
|
3070
|
+
maxTokens: Math.max(config.maxTokens, 3e3),
|
|
3071
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
3072
|
+
});
|
|
3073
|
+
notes.push(note.trim());
|
|
3074
|
+
onProgress?.(Math.min(i + BATCH, total), total, "章节摘要");
|
|
3075
|
+
}
|
|
3076
|
+
onProgress?.(total, total, "生成大纲");
|
|
3077
|
+
const outline = await complete(ctx, config, {
|
|
3078
|
+
system: [
|
|
3079
|
+
"你是一位经验丰富的小说主编。根据下面全书各章事件摘要,反推出这本书的总纲(大纲),可直接作为后续写作依据。要求:",
|
|
3080
|
+
"1. 第一行写《书名》(从摘要中的书名或内容推断,若无则用《未命名》)。",
|
|
3081
|
+
"2. 按故事弧线划分卷/部分:每卷给出卷名与主旨(标注覆盖章节范围)。",
|
|
3082
|
+
"3. 每一章列出:章节号 + 标题 + 一句话核心情节(若原章无标题可自拟)。",
|
|
3083
|
+
"4. 最后给出:全书主线、主要人物弧线、已埋设待回收的伏笔清单。",
|
|
3084
|
+
"5. 输出为纯文本 Markdown 结构(# 一级标题、## 二级标题、- 列表),不要多余寒暄。"
|
|
3085
|
+
].join("\n"),
|
|
3086
|
+
user: notes.join("\n\n"),
|
|
3087
|
+
temperature: .4,
|
|
3088
|
+
maxTokens: Math.max(config.maxTokens, 6e3),
|
|
3089
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
3090
|
+
});
|
|
3091
|
+
onProgress?.(total, total, "完成");
|
|
3092
|
+
return outline.trim();
|
|
3093
|
+
}
|
|
3094
|
+
/**
|
|
2919
3095
|
* 摘要 + 事实抽取合并为一次 LLM 调用(省一次调用与一次正文输入,
|
|
2920
3096
|
* 批量生成时整体开销约省 25%)。
|
|
2921
3097
|
* @returns 摘要与新增事实条数(失败返回空,调用方 best-effort)。
|
|
@@ -3120,7 +3296,8 @@ async function generateBlurb(ctx, config, project, partial = "") {
|
|
|
3120
3296
|
partial.trim() !== "" ? `已有开头草稿(请保留其内容与语气,续写补全为完整简介):\n${partial.trim()}` : "请全量生成一份完整简介。"
|
|
3121
3297
|
].filter((s) => s !== "").join("\n\n"),
|
|
3122
3298
|
temperature: .7,
|
|
3123
|
-
maxTokens: Math.max(config.maxTokens, 4e3)
|
|
3299
|
+
maxTokens: Math.max(config.maxTokens, 4e3),
|
|
3300
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
3124
3301
|
})).replace(/^["'「『]|["'」』]$/g, "").replace(/^简介[::]\s*/, "").trim().slice(0, 600);
|
|
3125
3302
|
}
|
|
3126
3303
|
/**
|
|
@@ -3282,7 +3459,8 @@ async function extractWorld(ctx, config, project) {
|
|
|
3282
3459
|
"只输出 JSON 对象。"
|
|
3283
3460
|
].filter((s) => s !== "").join("\n\n"),
|
|
3284
3461
|
temperature: .3,
|
|
3285
|
-
maxTokens: Math.max(config.maxTokens, 12e3)
|
|
3462
|
+
maxTokens: Math.max(config.maxTokens, 12e3),
|
|
3463
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
3286
3464
|
}));
|
|
3287
3465
|
const str = (value) => typeof value === "string" ? value.trim() : "";
|
|
3288
3466
|
const objArray = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "object" && v !== null) : [];
|
|
@@ -3421,7 +3599,8 @@ async function suggestForeshadows(ctx, config, project) {
|
|
|
3421
3599
|
system: foreshadowSystemPrompt(),
|
|
3422
3600
|
user,
|
|
3423
3601
|
temperature: .5,
|
|
3424
|
-
maxTokens: Math.max(config.maxTokens, 12e3)
|
|
3602
|
+
maxTokens: Math.max(config.maxTokens, 12e3),
|
|
3603
|
+
reasoning: config.analysisReasoning ?? "low"
|
|
3425
3604
|
}));
|
|
3426
3605
|
const existing = new Set(project.foreshadows.map((f) => f.description));
|
|
3427
3606
|
const created = [];
|
|
@@ -3989,6 +4168,8 @@ const NOVEL_API = {
|
|
|
3989
4168
|
bookshelfImportDir: "/api/dsh-novel-forge/bookshelf/import-dir",
|
|
3990
4169
|
/** 导入 txt/md 全本:拆章建项目并登记书架。 */
|
|
3991
4170
|
bookshelfImportText: "/api/dsh-novel-forge/bookshelf/import-text",
|
|
4171
|
+
/** 导入 txt/md 全本:拆章预览(不落盘)。 */
|
|
4172
|
+
bookshelfImportTextPreview: "/api/dsh-novel-forge/bookshelf/import-text/preview",
|
|
3992
4173
|
/** 重置项目(可选携带新大纲):清空设定/卷/章节/伏笔/资产/事实库。 */
|
|
3993
4174
|
reset: "/api/dsh-novel-forge/reset",
|
|
3994
4175
|
/** 全书一致性质检:LLM 扫描已生成章节,输出矛盾问题清单。 */
|
|
@@ -4023,6 +4204,8 @@ const NOVEL_API = {
|
|
|
4023
4204
|
sensitiveCheck: "/api/dsh-novel-forge/sensitive-check",
|
|
4024
4205
|
/** 开书想法 → AI 补全大纲:输入一句话想法,生成 2-3 个可选大纲方案。 */
|
|
4025
4206
|
outlineSuggest: "/api/dsh-novel-forge/outline/suggest",
|
|
4207
|
+
/** 反推大纲:从已写章节正文反向生成全书总纲(NDJSON 流)。 */
|
|
4208
|
+
outlineReverse: "/api/dsh-novel-forge/outline/reverse",
|
|
4026
4209
|
/** 拆书分析:对已写章节做结构/人物/文风/卖点四维体检(两阶段:源笔记→分节分析)。 */
|
|
4027
4210
|
breakdown: "/api/dsh-novel-forge/breakdown",
|
|
4028
4211
|
/** 漫剧分镜生成:章节 → 角色锚点 + 分镜表(可适配豆包/Seedance/SD)。 */
|
|
@@ -4829,7 +5012,7 @@ async function* runAssistantTurn(ctx, config, project, outputDir, userMessage) {
|
|
|
4829
5012
|
//#endregion
|
|
4830
5013
|
//#region src/routes.ts
|
|
4831
5014
|
/** Cap on JSON request bodies (generous: cover images travel as base64). */
|
|
4832
|
-
const MAX_JSON_BODY_BYTES =
|
|
5015
|
+
const MAX_JSON_BODY_BYTES = 64 * 1024 * 1024;
|
|
4833
5016
|
/** Loopback-only fence (mirrors the family plugins' pairing routes). */
|
|
4834
5017
|
function isLoopbackRequest(request) {
|
|
4835
5018
|
const address = request.socket.remoteAddress;
|
|
@@ -5862,23 +6045,34 @@ function makeRoutes(deps) {
|
|
|
5862
6045
|
}
|
|
5863
6046
|
}
|
|
5864
6047
|
};
|
|
5865
|
-
/** 导入 txt/md 全本(Mode B
|
|
6048
|
+
/** 导入 txt/md 全本(Mode B):拆章落盘建项目,登记书架。支持浏览器上传(text+fileName)与服务器本地文件(filePath)两种模式。 */
|
|
5866
6049
|
const bookshelfImportTextRoute = {
|
|
5867
6050
|
kind: "exact",
|
|
5868
6051
|
path: NOVEL_API.bookshelfImportText,
|
|
5869
6052
|
handler: async (req, res) => {
|
|
5870
6053
|
if (!guard(req, res, "POST")) return;
|
|
5871
6054
|
const body = await readJsonBody(req);
|
|
5872
|
-
const filePath = body?.filePath?.trim();
|
|
5873
|
-
if (filePath === void 0 || filePath === "") {
|
|
5874
|
-
writeJson(res, 400, { error: "filePath 不能为空" });
|
|
5875
|
-
return;
|
|
5876
|
-
}
|
|
5877
|
-
if (!existsSync(filePath)) {
|
|
5878
|
-
writeJson(res, 400, { error: `文件不存在:${filePath}` });
|
|
5879
|
-
return;
|
|
5880
|
-
}
|
|
5881
6055
|
try {
|
|
6056
|
+
if (body?.text !== void 0 && body.text.length > 0) {
|
|
6057
|
+
const bookName = (body.fileName ?? "").replace(/\.[^.]+$/, "").trim().slice(0, 40) || "导入小说";
|
|
6058
|
+
const outDir = body?.outputDir?.trim() !== void 0 && body.outputDir.trim() !== "" ? body.outputDir.trim() : defaultOutputDirFor(bookName);
|
|
6059
|
+
const result = importBookTextFromText(body.text, outDir, bookName);
|
|
6060
|
+
const { book } = importDir(outDir);
|
|
6061
|
+
writeJson(res, 200, {
|
|
6062
|
+
...result,
|
|
6063
|
+
book
|
|
6064
|
+
});
|
|
6065
|
+
return;
|
|
6066
|
+
}
|
|
6067
|
+
const filePath = body?.filePath?.trim();
|
|
6068
|
+
if (filePath === void 0 || filePath === "") {
|
|
6069
|
+
writeJson(res, 400, { error: "filePath 不能为空(或请上传文件内容)" });
|
|
6070
|
+
return;
|
|
6071
|
+
}
|
|
6072
|
+
if (!existsSync(filePath)) {
|
|
6073
|
+
writeJson(res, 400, { error: `文件不存在:${filePath}` });
|
|
6074
|
+
return;
|
|
6075
|
+
}
|
|
5882
6076
|
const bookName = basename(filePath, extname(filePath)).slice(0, 40) || "导入小说";
|
|
5883
6077
|
const outDir = body?.outputDir?.trim() !== void 0 && body.outputDir.trim() !== "" ? body.outputDir.trim() : defaultOutputDirFor(bookName);
|
|
5884
6078
|
const result = importBookText(filePath, outDir);
|
|
@@ -5892,6 +6086,27 @@ function makeRoutes(deps) {
|
|
|
5892
6086
|
}
|
|
5893
6087
|
}
|
|
5894
6088
|
};
|
|
6089
|
+
/** 上传全文做拆章预览(不落盘、不登记书架)。 */
|
|
6090
|
+
const bookshelfImportTextPreviewRoute = {
|
|
6091
|
+
kind: "exact",
|
|
6092
|
+
path: NOVEL_API.bookshelfImportTextPreview,
|
|
6093
|
+
handler: async (req, res) => {
|
|
6094
|
+
if (!guard(req, res, "POST")) return;
|
|
6095
|
+
const body = await readJsonBody(req);
|
|
6096
|
+
if (body?.text === void 0 || body.text.length === 0) {
|
|
6097
|
+
writeJson(res, 400, { error: "text 不能为空" });
|
|
6098
|
+
return;
|
|
6099
|
+
}
|
|
6100
|
+
try {
|
|
6101
|
+
writeJson(res, 200, {
|
|
6102
|
+
bookName: (body.fileName ?? "").replace(/\.[^.]+$/, "").trim().slice(0, 40) || "导入小说",
|
|
6103
|
+
...previewBookText(body.text)
|
|
6104
|
+
});
|
|
6105
|
+
} catch (err) {
|
|
6106
|
+
writeJson(res, 400, { error: err instanceof Error ? err.message : String(err) });
|
|
6107
|
+
}
|
|
6108
|
+
}
|
|
6109
|
+
};
|
|
5895
6110
|
/** 全书一致性质检。 */
|
|
5896
6111
|
const auditRoute = {
|
|
5897
6112
|
kind: "exact",
|
|
@@ -6673,6 +6888,52 @@ function makeRoutes(deps) {
|
|
|
6673
6888
|
}
|
|
6674
6889
|
}
|
|
6675
6890
|
};
|
|
6891
|
+
/** 反推大纲:从已写章节正文反向生成全书总纲(NDJSON 流式进度)。 */
|
|
6892
|
+
const outlineReverseRoute = {
|
|
6893
|
+
kind: "exact",
|
|
6894
|
+
path: NOVEL_API.outlineReverse,
|
|
6895
|
+
handler: async (req, res) => {
|
|
6896
|
+
if (!guard(req, res, "POST")) return;
|
|
6897
|
+
const config = getConfig();
|
|
6898
|
+
const project = requireProject(res);
|
|
6899
|
+
if (project === void 0) return;
|
|
6900
|
+
res.writeHead(200, {
|
|
6901
|
+
"content-type": "application/x-ndjson; charset=utf-8",
|
|
6902
|
+
"cache-control": "no-cache",
|
|
6903
|
+
"x-accel-buffering": "no",
|
|
6904
|
+
"referrer-policy": "no-referrer"
|
|
6905
|
+
});
|
|
6906
|
+
const send = (frame) => {
|
|
6907
|
+
res.write(JSON.stringify(frame) + "\n");
|
|
6908
|
+
};
|
|
6909
|
+
try {
|
|
6910
|
+
const outline = await reverseOutlineFromChapters(ctx, config, project, config.outputDir, (done, total, phase) => {
|
|
6911
|
+
send({
|
|
6912
|
+
type: "outline-progress",
|
|
6913
|
+
done,
|
|
6914
|
+
total,
|
|
6915
|
+
phase
|
|
6916
|
+
});
|
|
6917
|
+
});
|
|
6918
|
+
project.outline = outline;
|
|
6919
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6920
|
+
saveProject(config.outputDir, project);
|
|
6921
|
+
send({
|
|
6922
|
+
type: "outline-done",
|
|
6923
|
+
outline,
|
|
6924
|
+
chars: outline.length
|
|
6925
|
+
});
|
|
6926
|
+
} catch (error) {
|
|
6927
|
+
send({
|
|
6928
|
+
type: "error",
|
|
6929
|
+
no: 0,
|
|
6930
|
+
message: error.message
|
|
6931
|
+
});
|
|
6932
|
+
} finally {
|
|
6933
|
+
res.end();
|
|
6934
|
+
}
|
|
6935
|
+
}
|
|
6936
|
+
};
|
|
6676
6937
|
/** 拆书分析:对已写章节做结构/人物/文风/卖点体检(两阶段:源笔记→分节分析)。 */
|
|
6677
6938
|
const breakdownRoute = {
|
|
6678
6939
|
kind: "exact",
|
|
@@ -6817,6 +7078,7 @@ function makeRoutes(deps) {
|
|
|
6817
7078
|
bookshelfRemoveRoute,
|
|
6818
7079
|
bookshelfImportDirRoute,
|
|
6819
7080
|
bookshelfImportTextRoute,
|
|
7081
|
+
bookshelfImportTextPreviewRoute,
|
|
6820
7082
|
resetRoute,
|
|
6821
7083
|
auditRoute,
|
|
6822
7084
|
charactersRefreshRoute,
|
|
@@ -6837,6 +7099,7 @@ function makeRoutes(deps) {
|
|
|
6837
7099
|
configRoute,
|
|
6838
7100
|
openFolderRoute,
|
|
6839
7101
|
outlineSuggestRoute,
|
|
7102
|
+
outlineReverseRoute,
|
|
6840
7103
|
breakdownRoute,
|
|
6841
7104
|
{
|
|
6842
7105
|
kind: "exact",
|
|
@@ -6931,6 +7194,12 @@ const Config = z.object({
|
|
|
6931
7194
|
"high",
|
|
6932
7195
|
"max"
|
|
6933
7196
|
]).default("off"),
|
|
7197
|
+
analysisReasoning: z.union([
|
|
7198
|
+
"off",
|
|
7199
|
+
"low",
|
|
7200
|
+
"high",
|
|
7201
|
+
"max"
|
|
7202
|
+
]).default("low"),
|
|
6934
7203
|
chapterChars: z.number().default(3500),
|
|
6935
7204
|
maxTokens: z.number().default(12e3),
|
|
6936
7205
|
reviewPassScore: z.number().default(70),
|
|
@@ -6948,6 +7217,7 @@ const DEFAULT_OUTPUT_DIR = join(homedir(), ".dsh", "novels");
|
|
|
6948
7217
|
const DEFAULT_PROVIDER = "deepseek-official";
|
|
6949
7218
|
const DEFAULT_MODEL = "deepseek-v4-flash";
|
|
6950
7219
|
const DEFAULT_REASONING_EFFORT = "off";
|
|
7220
|
+
const DEFAULT_ANALYSIS_REASONING = "low";
|
|
6951
7221
|
const DEFAULT_CHAPTER_CHARS = 3500;
|
|
6952
7222
|
const DEFAULT_MAX_TOKENS = 12e3;
|
|
6953
7223
|
const DEFAULT_REVIEW_PASS_SCORE = 70;
|
|
@@ -6966,6 +7236,7 @@ function resolveConfig(value) {
|
|
|
6966
7236
|
provider: value?.provider ?? DEFAULT_PROVIDER,
|
|
6967
7237
|
model: value?.model ?? DEFAULT_MODEL,
|
|
6968
7238
|
reasoningEffort: value?.reasoningEffort ?? DEFAULT_REASONING_EFFORT,
|
|
7239
|
+
analysisReasoning: value?.analysisReasoning ?? DEFAULT_ANALYSIS_REASONING,
|
|
6969
7240
|
chapterChars: value?.chapterChars ?? DEFAULT_CHAPTER_CHARS,
|
|
6970
7241
|
maxTokens: value?.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
6971
7242
|
reviewPassScore: value?.reviewPassScore ?? DEFAULT_REVIEW_PASS_SCORE,
|
|
@@ -7000,6 +7271,7 @@ function apply(ctx, config) {
|
|
|
7000
7271
|
if (patch.provider !== void 0) next.provider = patch.provider;
|
|
7001
7272
|
if (patch.model !== void 0) next.model = patch.model;
|
|
7002
7273
|
if (patch.reasoningEffort !== void 0) next.reasoningEffort = patch.reasoningEffort;
|
|
7274
|
+
if (patch.analysisReasoning !== void 0) next.analysisReasoning = patch.analysisReasoning;
|
|
7003
7275
|
if (patch.chapterChars !== void 0) next.chapterChars = patch.chapterChars;
|
|
7004
7276
|
if (patch.maxTokens !== void 0) next.maxTokens = patch.maxTokens;
|
|
7005
7277
|
if (patch.reviewPassScore !== void 0) next.reviewPassScore = patch.reviewPassScore;
|