@waterwx/dsh-novel-forge 2.5.0 → 2.5.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/README.md +3 -3
- package/lib/client.js +1765 -1725
- package/lib/index.js +234 -106
- package/lib/types/assets.d.ts +2 -2
- package/lib/types/client/locales.d.ts +0 -1
- package/lib/types/client/panel/ChapterWorkbench.d.ts +69 -0
- package/lib/types/client/panel/NovelPanel.d.ts +0 -1
- package/lib/types/client/panel/WorkspaceView.d.ts +51 -0
- package/lib/types/client/panel/home-model.d.ts +53 -0
- package/lib/types/engine.d.ts +9 -1
- package/lib/types/index.d.ts +5 -1
- package/lib/types/protocol.d.ts +5 -1
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -278,10 +278,26 @@ function scanAiFlavor(text, rules) {
|
|
|
278
278
|
const totalChars = text.length;
|
|
279
279
|
const clicheHits = [];
|
|
280
280
|
let weightedTotal = 0;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
281
|
+
const occupied = new Uint8Array(text.length);
|
|
282
|
+
const patterns = Object.keys(PATTERN_COVERAGE).sort((a, b) => b.length - a.length);
|
|
283
|
+
for (const word of patterns) {
|
|
284
|
+
if (word.length === 0) continue;
|
|
284
285
|
const weight = patternWeight(PATTERN_COVERAGE[word] ?? 0);
|
|
286
|
+
let count = 0;
|
|
287
|
+
let at = text.indexOf(word);
|
|
288
|
+
while (at !== -1) {
|
|
289
|
+
let free = true;
|
|
290
|
+
for (let i = at; i < at + word.length; i++) if (occupied[i] === 1) {
|
|
291
|
+
free = false;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
if (free) {
|
|
295
|
+
count += 1;
|
|
296
|
+
for (let i = at; i < at + word.length; i++) occupied[i] = 1;
|
|
297
|
+
}
|
|
298
|
+
at = text.indexOf(word, at + word.length);
|
|
299
|
+
}
|
|
300
|
+
if (count === 0) continue;
|
|
285
301
|
clicheHits.push({
|
|
286
302
|
word,
|
|
287
303
|
count,
|
|
@@ -5718,7 +5734,8 @@ function buildCurveData(project) {
|
|
|
5718
5734
|
const a = actualTension(c);
|
|
5719
5735
|
if (t !== void 0) point.target = t;
|
|
5720
5736
|
if (a !== void 0) point.actual = a;
|
|
5721
|
-
|
|
5737
|
+
const refValue = preset === "custom" ? custom.length > 0 ? custom[c.no - 1] : t : reference[i];
|
|
5738
|
+
if (refValue !== void 0) point.reference = refValue;
|
|
5722
5739
|
return point;
|
|
5723
5740
|
})
|
|
5724
5741
|
};
|
|
@@ -8084,7 +8101,9 @@ async function authorReviewChapter(ctx, config, project, chapterNo, body, prevTa
|
|
|
8084
8101
|
system: authorReviewSystemPrompt(),
|
|
8085
8102
|
user,
|
|
8086
8103
|
temperature: .3,
|
|
8087
|
-
maxTokens: Math.max(config.maxTokens, 4e3)
|
|
8104
|
+
maxTokens: Math.max(config.maxTokens, 4e3),
|
|
8105
|
+
model: config.reviewModel,
|
|
8106
|
+
liveLabel: "作者复盘"
|
|
8088
8107
|
}));
|
|
8089
8108
|
const knownLineNames = new Set((project.plotlines ?? []).map((l) => l.name));
|
|
8090
8109
|
const advancedLines = Array.isArray(raw.advancedLines) ? raw.advancedLines.filter((n) => typeof n === "string" && n.trim() !== "" && knownLineNames.has(n.trim())).map((n) => n.trim()) : [];
|
|
@@ -8401,11 +8420,13 @@ async function extractRoles(ctx, config, project) {
|
|
|
8401
8420
|
/** AI 从全书提炼场景库:正文/编年录 → 高频重要场景的结构化视觉锚点。 */
|
|
8402
8421
|
function splitBookText(raw) {
|
|
8403
8422
|
const lines = raw.split(/\r?\n/);
|
|
8404
|
-
const chapterHead = /^\s*(?:#\s*)?第\s*(\d+|[
|
|
8423
|
+
const chapterHead = /^\s*(?:#\s*)?第\s*(\d+|[一二三四五六七八九十百千万零〇]+)\s*[章回节]\s*(.*?)\s*$/;
|
|
8405
8424
|
const cnOnlyHead = /^\s*(?:#\s*)?([一二三四五六七八九十百千万零〇]{1,6})(?:[、..::]\s*(.*?)\s*)?$/;
|
|
8406
8425
|
const specialHead = /^\s*(?:#\s*)?(序章|序言|楔子|引子|前言|开篇|尾声|终章|大结局|番外(?:篇|章)?|后记|完结感言|上架感言|作者的话)\s*[::、.\s]*(.*?)\s*$/;
|
|
8407
8426
|
const enHead = /^\s*(?:#\s*)?chapter\s+(\d+)\s*[.::、\-\s]*(.*?)\s*$/i;
|
|
8408
8427
|
const cnNum = {
|
|
8428
|
+
零: 0,
|
|
8429
|
+
〇: 0,
|
|
8409
8430
|
一: 1,
|
|
8410
8431
|
二: 2,
|
|
8411
8432
|
三: 3,
|
|
@@ -8417,7 +8438,8 @@ function splitBookText(raw) {
|
|
|
8417
8438
|
九: 9,
|
|
8418
8439
|
十: 10,
|
|
8419
8440
|
百: 100,
|
|
8420
|
-
千: 1e3
|
|
8441
|
+
千: 1e3,
|
|
8442
|
+
万: 1e4
|
|
8421
8443
|
};
|
|
8422
8444
|
const parseCn = (s) => {
|
|
8423
8445
|
if (/^\d+$/.test(s)) return Number(s);
|
|
@@ -8426,7 +8448,11 @@ function splitBookText(raw) {
|
|
|
8426
8448
|
for (const ch of s) {
|
|
8427
8449
|
const v = cnNum[ch];
|
|
8428
8450
|
if (v === void 0) return 0;
|
|
8429
|
-
if (v
|
|
8451
|
+
if (v === 0) continue;
|
|
8452
|
+
if (v >= 1e4) {
|
|
8453
|
+
section = (section + total) * v;
|
|
8454
|
+
total = 0;
|
|
8455
|
+
} else if (v >= 10) {
|
|
8430
8456
|
total += (section > 0 ? section : 1) * v;
|
|
8431
8457
|
section = 0;
|
|
8432
8458
|
} else section = v;
|
|
@@ -9795,12 +9821,12 @@ async function summarizeAndExtractFacts(ctx, config, project, outputDir, chapter
|
|
|
9795
9821
|
const summary = typeof raw.summary === "string" ? raw.summary.trim().slice(0, 500) : "";
|
|
9796
9822
|
const factLines = parseFactLines(raw.facts);
|
|
9797
9823
|
if (summary !== "") chapter.summary = summary;
|
|
9798
|
-
dedupAndAddFacts(project, chapterNo, factLines);
|
|
9824
|
+
const added = dedupAndAddFacts(project, chapterNo, factLines);
|
|
9799
9825
|
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9800
9826
|
saveProject(outputDir, project);
|
|
9801
9827
|
return {
|
|
9802
9828
|
summary,
|
|
9803
|
-
factCount:
|
|
9829
|
+
factCount: added
|
|
9804
9830
|
};
|
|
9805
9831
|
}
|
|
9806
9832
|
/**
|
|
@@ -9849,13 +9875,13 @@ async function summarizeFactsAndTimeline(ctx, config, project, outputDir, chapte
|
|
|
9849
9875
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
9850
9876
|
const extracted = parseTimelineItems(raw.timeline).map((item, index) => normalizeTimelineEvent(mapTimelineItem(item, index), chapterNo, index, now)).filter((e) => e.event !== "");
|
|
9851
9877
|
if (summary !== "") chapter.summary = summary;
|
|
9852
|
-
dedupAndAddFacts(project, chapterNo, factLines);
|
|
9878
|
+
const added = dedupAndAddFacts(project, chapterNo, factLines);
|
|
9853
9879
|
if (extracted.length > 0) project.timeline = [...(project.timeline ?? []).filter((e) => e.chapterNo !== chapterNo), ...extracted];
|
|
9854
9880
|
project.updatedAt = now;
|
|
9855
9881
|
saveProject(outputDir, project);
|
|
9856
9882
|
return {
|
|
9857
9883
|
summary,
|
|
9858
|
-
factCount:
|
|
9884
|
+
factCount: added,
|
|
9859
9885
|
timelineCount: extracted.length
|
|
9860
9886
|
};
|
|
9861
9887
|
}
|
|
@@ -10629,58 +10655,98 @@ async function reviewTimeline(ctx, config, project) {
|
|
|
10629
10655
|
const events = sortTimeline(project.timeline ?? []);
|
|
10630
10656
|
if (events.length === 0) return [];
|
|
10631
10657
|
const NL = String.fromCharCode(10);
|
|
10632
|
-
const
|
|
10633
|
-
|
|
10634
|
-
|
|
10635
|
-
|
|
10636
|
-
|
|
10637
|
-
|
|
10638
|
-
|
|
10639
|
-
|
|
10640
|
-
|
|
10641
|
-
|
|
10642
|
-
|
|
10643
|
-
|
|
10644
|
-
|
|
10645
|
-
|
|
10646
|
-
|
|
10647
|
-
|
|
10648
|
-
|
|
10649
|
-
|
|
10650
|
-
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10658
|
+
const system = timelineReviewSystemPrompt();
|
|
10659
|
+
const issues = [];
|
|
10660
|
+
const BATCH = 150;
|
|
10661
|
+
const totalBatches = Math.ceil(events.length / BATCH);
|
|
10662
|
+
for (let start = 0; start < events.length; start += BATCH) {
|
|
10663
|
+
const batch = events.slice(start, start + BATCH);
|
|
10664
|
+
const batchNo = Math.floor(start / BATCH) + 1;
|
|
10665
|
+
const text = await complete(ctx, config, {
|
|
10666
|
+
system,
|
|
10667
|
+
user: `故事时间线(按章节顺序;第 ${batchNo}/${totalBatches} 批):@NL@${batch.map((e) => {
|
|
10668
|
+
const parts = [`第${e.chapterNo}章`];
|
|
10669
|
+
if (e.order !== void 0) parts.push(`#${e.order}`);
|
|
10670
|
+
if (e.time !== "") parts.push(e.time);
|
|
10671
|
+
if (e.place !== "") parts.push(`@${e.place}`);
|
|
10672
|
+
if (e.characters.length > 0) parts.push(`(${e.characters.join("、")})`);
|
|
10673
|
+
parts.push(e.event);
|
|
10674
|
+
return "- " + parts.join(" ");
|
|
10675
|
+
}).join(NL)}`.replace("@NL@", NL),
|
|
10676
|
+
temperature: .2,
|
|
10677
|
+
maxTokens: Math.max(config.maxTokens, 4e3),
|
|
10678
|
+
model: config.auditModel || config.model,
|
|
10679
|
+
liveLabel: `时间线复核 · 第${batchNo}/${totalBatches}批`
|
|
10680
|
+
});
|
|
10681
|
+
let parsed = [];
|
|
10682
|
+
try {
|
|
10683
|
+
parsed = parseJson(text, true);
|
|
10684
|
+
} catch {
|
|
10685
|
+
parsed = [];
|
|
10686
|
+
}
|
|
10687
|
+
if (!Array.isArray(parsed)) continue;
|
|
10688
|
+
issues.push(...parsed.filter((item) => typeof item === "object" && item !== null).map((item) => {
|
|
10689
|
+
return {
|
|
10690
|
+
severity: item.severity === "high" || item.severity === "low" ? item.severity : "medium",
|
|
10691
|
+
chapters: Array.isArray(item.chapters) ? item.chapters.filter((c) => typeof c === "number" && Number.isFinite(c)) : [],
|
|
10692
|
+
item: typeof item.item === "string" ? item.item.slice(0, 300) : "",
|
|
10693
|
+
suggestion: typeof item.suggestion === "string" ? item.suggestion.slice(0, 300) : ""
|
|
10694
|
+
};
|
|
10695
|
+
}).filter((issue) => issue.item !== ""));
|
|
10654
10696
|
}
|
|
10655
|
-
|
|
10656
|
-
return parsed.filter((item) => typeof item === "object" && item !== null).map((item) => {
|
|
10657
|
-
return {
|
|
10658
|
-
severity: item.severity === "high" || item.severity === "low" ? item.severity : "medium",
|
|
10659
|
-
chapters: Array.isArray(item.chapters) ? item.chapters.filter((c) => typeof c === "number" && Number.isFinite(c)) : [],
|
|
10660
|
-
item: typeof item.item === "string" ? item.item.slice(0, 300) : "",
|
|
10661
|
-
suggestion: typeof item.suggestion === "string" ? item.suggestion.slice(0, 300) : ""
|
|
10662
|
-
};
|
|
10663
|
-
}).filter((issue) => issue.item !== "");
|
|
10697
|
+
return issues;
|
|
10664
10698
|
}
|
|
10665
|
-
/**
|
|
10699
|
+
/**
|
|
10700
|
+
* 选一个不覆盖已有文件的导出名:同名已存在时依次加 ` (2)`、` (3)`…
|
|
10701
|
+
*
|
|
10702
|
+
* 为什么不再固定文件名覆盖:作者往往「导出 → 看 → 改 → 再导出」,
|
|
10703
|
+
* 固定名会把上一版直接擦掉;导出件是成品,不该只有一份。
|
|
10704
|
+
* @param outputDir 书目录。
|
|
10705
|
+
* @param base 不含扩展名的文件名主体。
|
|
10706
|
+
* @param ext 扩展名(不带点)。
|
|
10707
|
+
*/
|
|
10708
|
+
function uniqueExportFile(outputDir, base, ext) {
|
|
10709
|
+
let file = `${base}.${ext}`;
|
|
10710
|
+
let n = 2;
|
|
10711
|
+
while (existsSync(join(outputDir, file))) {
|
|
10712
|
+
file = `${base} (${n}).${ext}`;
|
|
10713
|
+
n++;
|
|
10714
|
+
}
|
|
10715
|
+
return file;
|
|
10716
|
+
}
|
|
10717
|
+
/**
|
|
10718
|
+
* Export the whole book as one txt/md file.
|
|
10719
|
+
*
|
|
10720
|
+
* 只收真实存在且非空的章节文件:`chapter.file` 有值不等于文件没被手删/清空,
|
|
10721
|
+
* 旧实现会把它们当空串拼进去,导出件看起来完整、实际缺章。跳过的章号随
|
|
10722
|
+
* `skipped` 一起返回,由面板如实提示。
|
|
10723
|
+
*/
|
|
10666
10724
|
function exportBook(outputDir, project, format) {
|
|
10667
10725
|
const parts = [];
|
|
10668
10726
|
if (format === "md") parts.push(`# ${project.bookName}\n`);
|
|
10669
10727
|
else parts.push(project.bookName, "");
|
|
10670
|
-
const
|
|
10671
|
-
|
|
10672
|
-
|
|
10728
|
+
const written = [];
|
|
10729
|
+
const skipped = [];
|
|
10730
|
+
for (const chapter of project.chapters) {
|
|
10731
|
+
if (chapter.file === void 0 || chapter.file === "") continue;
|
|
10732
|
+
const body = readChapterFile(outputDir, chapter);
|
|
10733
|
+
if (body === void 0 || body.trim() === "") {
|
|
10734
|
+
skipped.push(chapter.no);
|
|
10735
|
+
continue;
|
|
10736
|
+
}
|
|
10737
|
+
written.push(chapter);
|
|
10673
10738
|
if (format === "md") parts.push(`\n## 第${chapter.no}章 ${chapter.title}\n`, body.trim(), "");
|
|
10674
10739
|
else parts.push("", `第${chapter.no}章 ${chapter.title}`, "", body.trim(), "");
|
|
10675
10740
|
}
|
|
10676
10741
|
const content = parts.join("\n");
|
|
10677
10742
|
const ext = format === "md" ? "md" : "txt";
|
|
10678
|
-
const file = `《${safeFileName(project.bookName)}
|
|
10743
|
+
const file = uniqueExportFile(outputDir, `《${safeFileName(project.bookName)}》全本`, ext);
|
|
10679
10744
|
writeFileSync(join(outputDir, file), content, "utf8");
|
|
10680
10745
|
return {
|
|
10681
10746
|
file,
|
|
10682
10747
|
chars: content.length,
|
|
10683
|
-
chapters:
|
|
10748
|
+
chapters: written.length,
|
|
10749
|
+
...skipped.length > 0 ? { skipped } : {}
|
|
10684
10750
|
};
|
|
10685
10751
|
}
|
|
10686
10752
|
/** 逐行渲染「- 名称:值」清单,跳过空值。 */
|
|
@@ -10857,7 +10923,7 @@ function exportScope(outputDir, project, scope, format) {
|
|
|
10857
10923
|
project
|
|
10858
10924
|
};
|
|
10859
10925
|
const content = JSON.stringify(payload, null, 2);
|
|
10860
|
-
const file = `《${safeFileName(project.bookName)}
|
|
10926
|
+
const file = uniqueExportFile(outputDir, `《${safeFileName(project.bookName)}》项目备份`, "json");
|
|
10861
10927
|
writeFileSync(join(outputDir, file), content, "utf8");
|
|
10862
10928
|
return {
|
|
10863
10929
|
file,
|
|
@@ -10868,7 +10934,7 @@ function exportScope(outputDir, project, scope, format) {
|
|
|
10868
10934
|
const body = scope === "settings" ? renderSettings(project) : scope === "plan" ? renderPlan(project) : scope === "characters" ? renderCharacters(project) : renderReview(project);
|
|
10869
10935
|
const ext = format === "json" ? "md" : format;
|
|
10870
10936
|
const label = scope === "settings" ? "项目设定" : scope === "plan" ? "规划" : scope === "characters" ? "角色" : "质检记录";
|
|
10871
|
-
const file = `《${safeFileName(project.bookName)}》${label}
|
|
10937
|
+
const file = uniqueExportFile(outputDir, `《${safeFileName(project.bookName)}》${label}`, ext);
|
|
10872
10938
|
writeFileSync(join(outputDir, file), body, "utf8");
|
|
10873
10939
|
return {
|
|
10874
10940
|
file,
|
|
@@ -11908,7 +11974,7 @@ function computeBookStage(project) {
|
|
|
11908
11974
|
allow: ["chapter_rewrite", "chapter_review"],
|
|
11909
11975
|
reason: `第 ${rejected.map((c) => c.no).join("、")} 章审稿未过,需要按意见修订`
|
|
11910
11976
|
};
|
|
11911
|
-
const awaitingReview = chapters.filter((c) => c.status === "written"
|
|
11977
|
+
const awaitingReview = chapters.filter((c) => c.status === "written");
|
|
11912
11978
|
if (awaitingReview.length > 0) return {
|
|
11913
11979
|
id: "review",
|
|
11914
11980
|
label: "审稿",
|
|
@@ -13198,11 +13264,15 @@ function upsertAuthorAsset(input) {
|
|
|
13198
13264
|
createdAt: prev.createdAt,
|
|
13199
13265
|
updatedAt: now
|
|
13200
13266
|
};
|
|
13201
|
-
} else
|
|
13202
|
-
|
|
13203
|
-
|
|
13204
|
-
|
|
13205
|
-
|
|
13267
|
+
} else {
|
|
13268
|
+
const id = input.id !== "" ? input.id : "aa-" + Date.now().toString(36) + "-" + randomBytes(4).toString("hex");
|
|
13269
|
+
library.items.push({
|
|
13270
|
+
...input,
|
|
13271
|
+
id,
|
|
13272
|
+
createdAt: input.createdAt !== "" ? input.createdAt : now,
|
|
13273
|
+
updatedAt: now
|
|
13274
|
+
});
|
|
13275
|
+
}
|
|
13206
13276
|
saveAuthorAssets(library);
|
|
13207
13277
|
return library;
|
|
13208
13278
|
}
|
|
@@ -13528,8 +13598,14 @@ async function scanMarketRanking(platforms) {
|
|
|
13528
13598
|
*/
|
|
13529
13599
|
const FILE = dshHomePath("novel-forge-global-assets.json");
|
|
13530
13600
|
let cache = null;
|
|
13601
|
+
/** 缓存对应的文件 mtime(ms);-1 = 文件不存在。 */
|
|
13602
|
+
let cacheMtime = -1;
|
|
13531
13603
|
function load() {
|
|
13532
|
-
|
|
13604
|
+
let mtime = -1;
|
|
13605
|
+
try {
|
|
13606
|
+
mtime = statSync(FILE).mtimeMs;
|
|
13607
|
+
} catch {}
|
|
13608
|
+
if (cache !== null && cacheMtime === mtime) return cache;
|
|
13533
13609
|
try {
|
|
13534
13610
|
const raw = readFileSync(FILE, "utf8");
|
|
13535
13611
|
const parsed = JSON.parse(raw);
|
|
@@ -13543,12 +13619,18 @@ function load() {
|
|
|
13543
13619
|
modes: []
|
|
13544
13620
|
};
|
|
13545
13621
|
}
|
|
13622
|
+
cacheMtime = mtime;
|
|
13546
13623
|
return cache;
|
|
13547
13624
|
}
|
|
13548
13625
|
function persist() {
|
|
13549
13626
|
if (cache === null) return;
|
|
13550
13627
|
mkdirSync(dshHome(), { recursive: true });
|
|
13551
13628
|
writeFileSync(FILE, JSON.stringify(cache, null, 2), "utf8");
|
|
13629
|
+
try {
|
|
13630
|
+
cacheMtime = statSync(FILE).mtimeMs;
|
|
13631
|
+
} catch {
|
|
13632
|
+
cacheMtime = -1;
|
|
13633
|
+
}
|
|
13552
13634
|
}
|
|
13553
13635
|
function globalGenreLibrary() {
|
|
13554
13636
|
return load().genres;
|
|
@@ -13728,6 +13810,26 @@ function isAbortError(err) {
|
|
|
13728
13810
|
/** Default chapter count for planning when the request omits it. */
|
|
13729
13811
|
const DEFAULT_PLAN_COUNT = 30;
|
|
13730
13812
|
/**
|
|
13813
|
+
* 复盘结果回灌编年录(与生产单 run.ts 同口径:stateChanges + clues)。
|
|
13814
|
+
*
|
|
13815
|
+
* 为什么必须回灌:作者复盘是**当前状态**的事实来源(谁伤了、谁拿到了什么、哪条线落地);
|
|
13816
|
+
* 只写 chapter.authorReview、不回灌 facts,会让面板单独补跑复盘的章节在后续生成里
|
|
13817
|
+
* 「失忆」——编年录缺了这段,下一章就可能写飞。
|
|
13818
|
+
*
|
|
13819
|
+
* @param project 项目状态(就地修改)。
|
|
13820
|
+
* @param chapterNo 章节号。
|
|
13821
|
+
* @param review 复盘结果。
|
|
13822
|
+
*/
|
|
13823
|
+
function backfillReviewFacts(project, chapterNo, review) {
|
|
13824
|
+
const facts = [...review.stateChanges ?? [], ...review.clues ?? []];
|
|
13825
|
+
if (facts.length === 0) return;
|
|
13826
|
+
project.facts ??= [];
|
|
13827
|
+
for (const text of facts) project.facts.push({
|
|
13828
|
+
chapterNo,
|
|
13829
|
+
text
|
|
13830
|
+
});
|
|
13831
|
+
}
|
|
13832
|
+
/**
|
|
13731
13833
|
* The profile this host booted (`--profile <name>` on the dsh CLI invocation).
|
|
13732
13834
|
* Profile-scoped paths must not assume a name: the shipped template is `web`,
|
|
13733
13835
|
* but a custom profile (`web-alpha`) is the one actually running.
|
|
@@ -14136,7 +14238,8 @@ function makeRoutes(deps) {
|
|
|
14136
14238
|
const review = await authorReviewChapter(ctx, config, project, no, currentBody, prevTail);
|
|
14137
14239
|
chapter.authorReview = review;
|
|
14138
14240
|
if (review.advancedLines !== void 0) autoLinkPlotlines(project, no, review.advancedLines);
|
|
14139
|
-
|
|
14241
|
+
backfillReviewFacts(project, no, review);
|
|
14242
|
+
mergeVolatileFromDisk(config.outputDir, project, ["plotlines", "facts"]);
|
|
14140
14243
|
saveProject(config.outputDir, project);
|
|
14141
14244
|
send({
|
|
14142
14245
|
type: "author-review",
|
|
@@ -14322,9 +14425,9 @@ function makeRoutes(deps) {
|
|
|
14322
14425
|
handler: async (req, res) => {
|
|
14323
14426
|
if (!guard(req, res, "POST")) return;
|
|
14324
14427
|
const config = getConfig();
|
|
14325
|
-
const project = requireProjectAt(req, res);
|
|
14326
|
-
if (project === void 0) return;
|
|
14327
14428
|
const body = await readJsonBody(req);
|
|
14429
|
+
const project = requireProjectAt(req, res, body);
|
|
14430
|
+
if (project === void 0) return;
|
|
14328
14431
|
if (!Number.isInteger(body?.chapterNo)) {
|
|
14329
14432
|
writeJson(res, 400, { error: "chapterNo 须为正整数" });
|
|
14330
14433
|
return;
|
|
@@ -14373,9 +14476,9 @@ function makeRoutes(deps) {
|
|
|
14373
14476
|
handler: async (req, res) => {
|
|
14374
14477
|
if (!guard(req, res, "POST")) return;
|
|
14375
14478
|
const config = getConfig();
|
|
14376
|
-
const project = requireProjectAt(req, res);
|
|
14377
|
-
if (project === void 0) return;
|
|
14378
14479
|
const body = await readJsonBody(req);
|
|
14480
|
+
const project = requireProjectAt(req, res, body);
|
|
14481
|
+
if (project === void 0) return;
|
|
14379
14482
|
if (!Number.isInteger(body?.chapterNo)) {
|
|
14380
14483
|
writeJson(res, 400, { error: "chapterNo 须为正整数" });
|
|
14381
14484
|
return;
|
|
@@ -14399,9 +14502,9 @@ function makeRoutes(deps) {
|
|
|
14399
14502
|
handler: async (req, res) => {
|
|
14400
14503
|
if (!guard(req, res, "POST")) return;
|
|
14401
14504
|
const config = getConfig();
|
|
14402
|
-
const project = requireProjectAt(req, res);
|
|
14403
|
-
if (project === void 0) return;
|
|
14404
14505
|
const body = await readJsonBody(req);
|
|
14506
|
+
const project = requireProjectAt(req, res, body);
|
|
14507
|
+
if (project === void 0) return;
|
|
14405
14508
|
if (!Number.isInteger(body?.chapterNo)) {
|
|
14406
14509
|
writeJson(res, 400, { error: "chapterNo 须为正整数" });
|
|
14407
14510
|
return;
|
|
@@ -14543,9 +14646,9 @@ function makeRoutes(deps) {
|
|
|
14543
14646
|
handler: async (req, res) => {
|
|
14544
14647
|
if (!guard(req, res, "POST")) return;
|
|
14545
14648
|
const config = getConfig();
|
|
14546
|
-
const project = requireProjectAt(req, res);
|
|
14547
|
-
if (project === void 0) return;
|
|
14548
14649
|
const body = await readJsonBody(req);
|
|
14650
|
+
const project = requireProjectAt(req, res, body);
|
|
14651
|
+
if (project === void 0) return;
|
|
14549
14652
|
const text = body?.text?.trim() ?? "";
|
|
14550
14653
|
if (text.length < 50) {
|
|
14551
14654
|
writeJson(res, 400, { error: "正文过短(<50 字),请先编辑内容" });
|
|
@@ -14565,9 +14668,9 @@ function makeRoutes(deps) {
|
|
|
14565
14668
|
handler: async (req, res) => {
|
|
14566
14669
|
if (!guard(req, res, "POST")) return;
|
|
14567
14670
|
const config = getConfig();
|
|
14568
|
-
const project = requireProjectAt(req, res);
|
|
14569
|
-
if (project === void 0) return;
|
|
14570
14671
|
const body = await readJsonBody(req);
|
|
14672
|
+
const project = requireProjectAt(req, res, body);
|
|
14673
|
+
if (project === void 0) return;
|
|
14571
14674
|
if (!Number.isInteger(body?.chapterNo)) {
|
|
14572
14675
|
writeJson(res, 400, { error: "chapterNo 须为正整数" });
|
|
14573
14676
|
return;
|
|
@@ -16026,7 +16129,8 @@ function makeRoutes(deps) {
|
|
|
16026
16129
|
handler: async (req, res) => {
|
|
16027
16130
|
if (!guard(req, res, "POST")) return;
|
|
16028
16131
|
const config = getConfig();
|
|
16029
|
-
const
|
|
16132
|
+
const body = await readJsonBody(req);
|
|
16133
|
+
const project = requireProjectAt(req, res, body);
|
|
16030
16134
|
if (project === void 0) return;
|
|
16031
16135
|
try {
|
|
16032
16136
|
const cards = await refreshCharacters(ctx, config, project, config.outputDir);
|
|
@@ -16046,7 +16150,8 @@ function makeRoutes(deps) {
|
|
|
16046
16150
|
handler: async (req, res) => {
|
|
16047
16151
|
if (!guard(req, res, "POST")) return;
|
|
16048
16152
|
const config = getConfig();
|
|
16049
|
-
const
|
|
16153
|
+
const body = await readJsonBody(req);
|
|
16154
|
+
const project = requireProjectAt(req, res, body);
|
|
16050
16155
|
if (project === void 0) return;
|
|
16051
16156
|
try {
|
|
16052
16157
|
writeJson(res, 200, {
|
|
@@ -16065,13 +16170,13 @@ function makeRoutes(deps) {
|
|
|
16065
16170
|
handler: async (req, res) => {
|
|
16066
16171
|
if (!guard(req, res, "POST")) return;
|
|
16067
16172
|
const config = getConfig();
|
|
16068
|
-
const
|
|
16173
|
+
const body = await readJsonBody(req);
|
|
16174
|
+
const project = requireProjectAt(req, res, body);
|
|
16069
16175
|
if (project === void 0) return;
|
|
16070
16176
|
if (project.bible === void 0) {
|
|
16071
16177
|
writeJson(res, 400, { error: "尚未生成道藏,请先生成" });
|
|
16072
16178
|
return;
|
|
16073
16179
|
}
|
|
16074
|
-
const body = await readJsonBody(req);
|
|
16075
16180
|
if (Array.isArray(body?.worldRules)) project.bible.worldRules = body.worldRules.filter((r) => r.trim() !== "");
|
|
16076
16181
|
if (Array.isArray(body?.redLines)) project.bible.redLines = body.redLines.filter((r) => r.trim() !== "");
|
|
16077
16182
|
if (Array.isArray(body?.style)) project.bible.style = body.style.filter((r) => r.trim() !== "");
|
|
@@ -16100,9 +16205,9 @@ function makeRoutes(deps) {
|
|
|
16100
16205
|
handler: async (req, res) => {
|
|
16101
16206
|
if (!guard(req, res, "POST")) return;
|
|
16102
16207
|
const config = getConfig();
|
|
16103
|
-
const project = requireProjectAt(req, res);
|
|
16104
|
-
if (project === void 0) return;
|
|
16105
16208
|
const body = await readJsonBody(req);
|
|
16209
|
+
const project = requireProjectAt(req, res, body);
|
|
16210
|
+
if (project === void 0) return;
|
|
16106
16211
|
try {
|
|
16107
16212
|
if (body?.action === "save") {
|
|
16108
16213
|
const text = body.text?.trim() ?? "";
|
|
@@ -16154,9 +16259,9 @@ function makeRoutes(deps) {
|
|
|
16154
16259
|
}
|
|
16155
16260
|
if (req.method === "POST") {
|
|
16156
16261
|
if (!guard(req, res, "POST")) return;
|
|
16157
|
-
const project = requireProjectAt(req, res);
|
|
16158
|
-
if (project === void 0) return;
|
|
16159
16262
|
const body = await readJsonBody(req);
|
|
16263
|
+
const project = requireProjectAt(req, res, body);
|
|
16264
|
+
if (project === void 0) return;
|
|
16160
16265
|
try {
|
|
16161
16266
|
if (body?.action === "remove") {
|
|
16162
16267
|
if (project.coverPath !== void 0 && project.coverPath !== "") {
|
|
@@ -16210,9 +16315,9 @@ function makeRoutes(deps) {
|
|
|
16210
16315
|
handler: async (req, res) => {
|
|
16211
16316
|
if (!guard(req, res, "POST")) return;
|
|
16212
16317
|
const config = getConfig();
|
|
16213
|
-
const project = requireProjectAt(req, res);
|
|
16214
|
-
if (project === void 0) return;
|
|
16215
16318
|
const body = await readJsonBody(req);
|
|
16319
|
+
const project = requireProjectAt(req, res, body);
|
|
16320
|
+
if (project === void 0) return;
|
|
16216
16321
|
try {
|
|
16217
16322
|
if (body?.action === "save" && body.world !== void 0) {
|
|
16218
16323
|
project.world = {
|
|
@@ -16242,9 +16347,10 @@ function makeRoutes(deps) {
|
|
|
16242
16347
|
handler: async (req, res) => {
|
|
16243
16348
|
if (!guard(req, res, "POST")) return;
|
|
16244
16349
|
const config = getConfig();
|
|
16245
|
-
const
|
|
16350
|
+
const body = await readJsonBody(req);
|
|
16351
|
+
const project = requireProjectAt(req, res, body);
|
|
16246
16352
|
if (project === void 0) return;
|
|
16247
|
-
const bookName =
|
|
16353
|
+
const bookName = body?.bookName?.trim();
|
|
16248
16354
|
if (bookName === void 0 || bookName === "") {
|
|
16249
16355
|
writeJson(res, 400, { error: "书名不能为空" });
|
|
16250
16356
|
return;
|
|
@@ -16264,9 +16370,9 @@ function makeRoutes(deps) {
|
|
|
16264
16370
|
handler: async (req, res) => {
|
|
16265
16371
|
if (!guard(req, res, "POST")) return;
|
|
16266
16372
|
const config = getConfig();
|
|
16267
|
-
const project = requireProjectAt(req, res);
|
|
16268
|
-
if (project === void 0) return;
|
|
16269
16373
|
const body = await readJsonBody(req);
|
|
16374
|
+
const project = requireProjectAt(req, res, body);
|
|
16375
|
+
if (project === void 0) return;
|
|
16270
16376
|
if (project.plotlines === void 0) project.plotlines = [];
|
|
16271
16377
|
const op = body?.op;
|
|
16272
16378
|
if (op === "add" && body?.line !== void 0) {
|
|
@@ -16393,9 +16499,9 @@ function makeRoutes(deps) {
|
|
|
16393
16499
|
handler: async (req, res) => {
|
|
16394
16500
|
if (!guard(req, res, "POST")) return;
|
|
16395
16501
|
const config = getConfig();
|
|
16396
|
-
const project = requireProjectAt(req, res);
|
|
16397
|
-
if (project === void 0) return;
|
|
16398
16502
|
const body = await readJsonBody(req);
|
|
16503
|
+
const project = requireProjectAt(req, res, body);
|
|
16504
|
+
if (project === void 0) return;
|
|
16399
16505
|
const hits = [];
|
|
16400
16506
|
let scanned = 0;
|
|
16401
16507
|
if (body?.text !== void 0) for (const hit of checkSensitiveText(body.text)) hits.push({
|
|
@@ -16447,9 +16553,9 @@ function makeRoutes(deps) {
|
|
|
16447
16553
|
handler: async (req, res) => {
|
|
16448
16554
|
if (!guard(req, res, "POST")) return;
|
|
16449
16555
|
const config = getConfig();
|
|
16450
|
-
const project = requireProjectAt(req, res);
|
|
16451
|
-
if (project === void 0) return;
|
|
16452
16556
|
const body = await readJsonBody(req);
|
|
16557
|
+
const project = requireProjectAt(req, res, body);
|
|
16558
|
+
if (project === void 0) return;
|
|
16453
16559
|
if (project.roles === void 0) project.roles = [];
|
|
16454
16560
|
const op = body?.op;
|
|
16455
16561
|
if (op === "extract") try {
|
|
@@ -16485,9 +16591,10 @@ function makeRoutes(deps) {
|
|
|
16485
16591
|
handler: async (req, res) => {
|
|
16486
16592
|
if (!guard(req, res, "POST")) return;
|
|
16487
16593
|
const config = getConfig();
|
|
16488
|
-
const
|
|
16594
|
+
const body = await readJsonBody(req);
|
|
16595
|
+
const project = requireProjectAt(req, res, body);
|
|
16489
16596
|
if (project === void 0) return;
|
|
16490
|
-
const no =
|
|
16597
|
+
const no = body?.chapterNo;
|
|
16491
16598
|
if (!Number.isInteger(no) || no === void 0 || no < 1) {
|
|
16492
16599
|
writeJson(res, 400, { error: "chapterNo 须为正整数" });
|
|
16493
16600
|
return;
|
|
@@ -16515,9 +16622,10 @@ function makeRoutes(deps) {
|
|
|
16515
16622
|
handler: async (req, res) => {
|
|
16516
16623
|
if (!guard(req, res, "POST")) return;
|
|
16517
16624
|
const config = getConfig();
|
|
16518
|
-
const
|
|
16625
|
+
const body = await readJsonBody(req);
|
|
16626
|
+
const project = requireProjectAt(req, res, body);
|
|
16519
16627
|
if (project === void 0) return;
|
|
16520
|
-
const no =
|
|
16628
|
+
const no = body?.chapterNo;
|
|
16521
16629
|
if (!Number.isInteger(no) || no === void 0 || no < 1) {
|
|
16522
16630
|
writeJson(res, 400, { error: "chapterNo 须为正整数" });
|
|
16523
16631
|
return;
|
|
@@ -16549,9 +16657,9 @@ function makeRoutes(deps) {
|
|
|
16549
16657
|
handler: async (req, res) => {
|
|
16550
16658
|
if (!guard(req, res, "POST")) return;
|
|
16551
16659
|
const config = getConfig();
|
|
16552
|
-
const project = requireProjectAt(req, res);
|
|
16553
|
-
if (project === void 0) return;
|
|
16554
16660
|
const body = await readJsonBody(req);
|
|
16661
|
+
const project = requireProjectAt(req, res, body);
|
|
16662
|
+
if (project === void 0) return;
|
|
16555
16663
|
const no = body?.chapterNo;
|
|
16556
16664
|
if (!Number.isInteger(no) || no === void 0 || no < 1) {
|
|
16557
16665
|
writeJson(res, 400, { error: "chapterNo 须为正整数" });
|
|
@@ -16610,9 +16718,9 @@ function makeRoutes(deps) {
|
|
|
16610
16718
|
handler: async (req, res) => {
|
|
16611
16719
|
if (!guard(req, res, "POST")) return;
|
|
16612
16720
|
const config = getConfig();
|
|
16613
|
-
const project = requireProjectAt(req, res);
|
|
16614
|
-
if (project === void 0) return;
|
|
16615
16721
|
const body = await readJsonBody(req);
|
|
16722
|
+
const project = requireProjectAt(req, res, body);
|
|
16723
|
+
if (project === void 0) return;
|
|
16616
16724
|
const abort = abortOnClientClose(req, res);
|
|
16617
16725
|
/** 对一章执行作者复盘(读取已落盘正文,不改变章节状态/正文)。 */
|
|
16618
16726
|
const runOne = async (chapter) => {
|
|
@@ -16640,6 +16748,7 @@ function makeRoutes(deps) {
|
|
|
16640
16748
|
const review = await runOne(chapter);
|
|
16641
16749
|
chapter.authorReview = review;
|
|
16642
16750
|
if (review.advancedLines !== void 0) autoLinkPlotlines(project, chapter.no, review.advancedLines);
|
|
16751
|
+
backfillReviewFacts(project, chapter.no, review);
|
|
16643
16752
|
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
16644
16753
|
saveProject(config.outputDir, project);
|
|
16645
16754
|
writeJson(res, 200, {
|
|
@@ -16670,6 +16779,7 @@ function makeRoutes(deps) {
|
|
|
16670
16779
|
const review = await runOne(chapter);
|
|
16671
16780
|
chapter.authorReview = review;
|
|
16672
16781
|
if (review.advancedLines !== void 0) autoLinkPlotlines(project, chapter.no, review.advancedLines);
|
|
16782
|
+
backfillReviewFacts(project, chapter.no, review);
|
|
16673
16783
|
done++;
|
|
16674
16784
|
saveProject(config.outputDir, project);
|
|
16675
16785
|
send({
|
|
@@ -16798,7 +16908,8 @@ function makeRoutes(deps) {
|
|
|
16798
16908
|
handler: async (req, res) => {
|
|
16799
16909
|
if (!guard(req, res, "POST")) return;
|
|
16800
16910
|
const config = getConfig();
|
|
16801
|
-
const
|
|
16911
|
+
const body = await readJsonBody(req);
|
|
16912
|
+
const project = requireProjectAt(req, res, body);
|
|
16802
16913
|
if (project === void 0) return;
|
|
16803
16914
|
res.writeHead(200, {
|
|
16804
16915
|
"content-type": "application/x-ndjson; charset=utf-8",
|
|
@@ -16939,9 +17050,9 @@ function makeRoutes(deps) {
|
|
|
16939
17050
|
handler: async (req, res) => {
|
|
16940
17051
|
if (!guard(req, res, "POST")) return;
|
|
16941
17052
|
const config = getConfig();
|
|
16942
|
-
const project = requireProjectAt(req, res);
|
|
16943
|
-
if (project === void 0) return;
|
|
16944
17053
|
const body = await readJsonBody(req);
|
|
17054
|
+
const project = requireProjectAt(req, res, body);
|
|
17055
|
+
if (project === void 0) return;
|
|
16945
17056
|
try {
|
|
16946
17057
|
writeJson(res, 200, await breakdownBook(ctx, config, project, config.outputDir, body?.scope ?? "recent", body?.preset ?? "quick", body?.budgetTokens ?? 5e4));
|
|
16947
17058
|
} catch (error) {
|
|
@@ -17202,11 +17313,18 @@ function makeRoutes(deps) {
|
|
|
17202
17313
|
const body = await readJsonBody(req);
|
|
17203
17314
|
const config = getConfig();
|
|
17204
17315
|
let text = body?.text?.trim() ?? "";
|
|
17205
|
-
if (text === "" && body?.filePath !== void 0 && body.filePath !== "")
|
|
17206
|
-
|
|
17207
|
-
|
|
17208
|
-
|
|
17209
|
-
|
|
17316
|
+
if (text === "" && body?.filePath !== void 0 && body.filePath !== "") {
|
|
17317
|
+
const sourcePath = body.filePath.trim();
|
|
17318
|
+
if (!IMPORTABLE_EXTENSIONS.includes(extname(sourcePath).toLowerCase())) {
|
|
17319
|
+
writeJson(res, 400, { error: "只支持 txt / md / markdown 文件" });
|
|
17320
|
+
return;
|
|
17321
|
+
}
|
|
17322
|
+
try {
|
|
17323
|
+
text = readFileSync(sourcePath, "utf8");
|
|
17324
|
+
} catch (err) {
|
|
17325
|
+
writeJson(res, 400, { error: "读取文件失败:" + err.message });
|
|
17326
|
+
return;
|
|
17327
|
+
}
|
|
17210
17328
|
}
|
|
17211
17329
|
if (text.length < 200) {
|
|
17212
17330
|
writeJson(res, 400, { error: "全文内容过短(<200 字符),请上传完整小说文本" });
|
|
@@ -17780,6 +17898,7 @@ const Config = z.object({
|
|
|
17780
17898
|
reviewModel: live(z.string().default("")),
|
|
17781
17899
|
auditModel: live(z.string().default("")),
|
|
17782
17900
|
fallbackModel: live(z.string().default("")),
|
|
17901
|
+
fallbackMaxTokens: live(z.number().default(0)),
|
|
17783
17902
|
autoTimeline: live(z.boolean().default(true)),
|
|
17784
17903
|
reasoningEffort: live(z.union([
|
|
17785
17904
|
"off",
|
|
@@ -17802,7 +17921,13 @@ const Config = z.object({
|
|
|
17802
17921
|
themeBackground: live(z.string().default("")),
|
|
17803
17922
|
themeBackgroundBlur: live(z.number().default(0)),
|
|
17804
17923
|
themeOpacity: live(z.number().default(100)),
|
|
17805
|
-
enableAdaptMode: live(z.boolean().default(true))
|
|
17924
|
+
enableAdaptMode: live(z.boolean().default(true)),
|
|
17925
|
+
savedModels: live(z.array(z.object({
|
|
17926
|
+
id: z.string(),
|
|
17927
|
+
name: z.string(),
|
|
17928
|
+
provider: z.string(),
|
|
17929
|
+
model: z.string()
|
|
17930
|
+
})).default([]))
|
|
17806
17931
|
});
|
|
17807
17932
|
/** Schema defaults, re-read for hand-built test contexts. */
|
|
17808
17933
|
const DEFAULT_ANNOUNCE = true;
|
|
@@ -17835,6 +17960,7 @@ function resolveConfig(value) {
|
|
|
17835
17960
|
reviewModel: value?.reviewModel,
|
|
17836
17961
|
auditModel: value?.auditModel,
|
|
17837
17962
|
fallbackModel: value?.fallbackModel ?? DEFAULT_FALLBACK_MODEL,
|
|
17963
|
+
fallbackMaxTokens: value?.fallbackMaxTokens,
|
|
17838
17964
|
autoTimeline: value?.autoTimeline ?? DEFAULT_AUTO_TIMELINE,
|
|
17839
17965
|
reasoningEffort: value?.reasoningEffort ?? DEFAULT_REASONING_EFFORT,
|
|
17840
17966
|
analysisReasoning: value?.analysisReasoning ?? DEFAULT_ANALYSIS_REASONING,
|
|
@@ -17847,7 +17973,8 @@ function resolveConfig(value) {
|
|
|
17847
17973
|
themeBackground: value?.themeBackground ?? "",
|
|
17848
17974
|
themeBackgroundBlur: value?.themeBackgroundBlur ?? 0,
|
|
17849
17975
|
themeOpacity: value?.themeOpacity ?? 100,
|
|
17850
|
-
enableAdaptMode: value?.enableAdaptMode ?? true
|
|
17976
|
+
enableAdaptMode: value?.enableAdaptMode ?? true,
|
|
17977
|
+
savedModels: value?.savedModels ?? []
|
|
17851
17978
|
};
|
|
17852
17979
|
}
|
|
17853
17980
|
/**
|
|
@@ -17910,6 +18037,7 @@ function apply(ctx, config) {
|
|
|
17910
18037
|
if (patch.reviewModel !== void 0) next.reviewModel = patch.reviewModel;
|
|
17911
18038
|
if (patch.auditModel !== void 0) next.auditModel = patch.auditModel;
|
|
17912
18039
|
if (patch.fallbackModel !== void 0) next.fallbackModel = patch.fallbackModel;
|
|
18040
|
+
if (patch.fallbackMaxTokens !== void 0) next.fallbackMaxTokens = patch.fallbackMaxTokens;
|
|
17913
18041
|
if (patch.autoTimeline !== void 0) next.autoTimeline = patch.autoTimeline;
|
|
17914
18042
|
if (patch.reasoningEffort !== void 0) next.reasoningEffort = patch.reasoningEffort;
|
|
17915
18043
|
if (patch.analysisReasoning !== void 0) next.analysisReasoning = patch.analysisReasoning;
|
|
@@ -17923,6 +18051,7 @@ function apply(ctx, config) {
|
|
|
17923
18051
|
if (patch.themeBackgroundBlur !== void 0) next.themeBackgroundBlur = patch.themeBackgroundBlur;
|
|
17924
18052
|
if (patch.themeOpacity !== void 0) next.themeOpacity = patch.themeOpacity;
|
|
17925
18053
|
if (patch.enableAdaptMode !== void 0) next.enableAdaptMode = patch.enableAdaptMode;
|
|
18054
|
+
if (patch.savedModels !== void 0) next.savedModels = patch.savedModels;
|
|
17926
18055
|
const settings = ctx.get("settings");
|
|
17927
18056
|
const persisted = await persistConfigPatch(current(), next, settings, settingsNs, NOVEL_SETTINGS_NAMESPACE);
|
|
17928
18057
|
setCurrent(persisted.config);
|
|
@@ -17988,7 +18117,6 @@ function apply(ctx, config) {
|
|
|
17988
18117
|
},
|
|
17989
18118
|
onChange: sync
|
|
17990
18119
|
});
|
|
17991
|
-
ctx.get("skills");
|
|
17992
18120
|
sync();
|
|
17993
18121
|
}
|
|
17994
18122
|
//#endregion
|