@waterwx/dsh-novel-forge 1.1.4 → 1.1.5
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 +2 -0
- package/lib/client.js +815 -287
- package/lib/client.js.map +1 -1
- package/lib/index.js +740 -521
- package/lib/index.js.map +1 -1
- package/lib/types/client/api.d.ts +7 -0
- package/lib/types/client/panel/NovelPanel.d.ts +1 -1
- package/lib/types/engine.d.ts +3 -1
- package/lib/types/protocol.d.ts +45 -0
- package/package.json +1 -1
- package/src/assistant.ts +27 -4
- package/src/client/api.ts +10 -0
- package/src/client/panel/NovelPanel.tsx +279 -41
- package/src/engine.ts +77 -0
- package/src/protocol.ts +48 -0
- package/src/routes.ts +90 -0
package/lib/index.js
CHANGED
|
@@ -65,8 +65,12 @@ const NOVEL_API = {
|
|
|
65
65
|
cover: "/api/dsh-novel-forge/blurb/cover",
|
|
66
66
|
/** 剧情线管理:增删改 + 关联章节。 */
|
|
67
67
|
plotlines: "/api/dsh-novel-forge/plotlines",
|
|
68
|
+
/** 角色库:AI 提炼 / 采纳 / 更新 / 删除。 */
|
|
69
|
+
roles: "/api/dsh-novel-forge/roles",
|
|
68
70
|
/** 作者复盘补跑:对已写章节补齐 authorReview(全书流式 / 单章 JSON)。 */
|
|
69
71
|
reviewBackfill: "/api/dsh-novel-forge/review/backfill",
|
|
72
|
+
/** 章节复位:generating 卡死 → pending(可重新生成)。 */
|
|
73
|
+
chapterReset: "/api/dsh-novel-forge/chapter/reset",
|
|
70
74
|
/** 敏感词检查:全书已写章节或指定文本。 */
|
|
71
75
|
sensitiveCheck: "/api/dsh-novel-forge/sensitive-check",
|
|
72
76
|
config: "/api/dsh-novel-forge/config",
|
|
@@ -1426,6 +1430,19 @@ function writeSystemPrompt(project) {
|
|
|
1426
1430
|
if (Array.isArray(card.knowledge) && card.knowledge.length > 0) sections.push(` 已知信息(该角色知道的:${card.knowledge.join(";")};未列出的信息该角色一律不知道,不得写其知晓或提及)`);
|
|
1427
1431
|
}
|
|
1428
1432
|
}
|
|
1433
|
+
const roleLib = project.roles ?? [];
|
|
1434
|
+
if (roleLib.length > 0) {
|
|
1435
|
+
const labelName = {
|
|
1436
|
+
protagonist: "主角",
|
|
1437
|
+
female_lead: "女主",
|
|
1438
|
+
female_support: "女配",
|
|
1439
|
+
support: "配角",
|
|
1440
|
+
antagonist: "反派",
|
|
1441
|
+
extra: "路人"
|
|
1442
|
+
};
|
|
1443
|
+
sections.push("角色库(出场角色按定位规格刻画互动):");
|
|
1444
|
+
for (const r of roleLib) sections.push(`- ${r.name}(${labelName[r.roleLabel]}):${r.identity}${r.relations.length > 0 ? `;关系:${r.relations.join("、")}` : ""}`);
|
|
1445
|
+
}
|
|
1429
1446
|
if (bible.redLines.length > 0) sections.push("写作红线(违反即失败):\n" + bible.redLines.map((r) => `- ${r}`).join("\n"));
|
|
1430
1447
|
if (bible.style.length > 0) sections.push("风格要求:\n" + bible.style.map((r) => `- ${r}`).join("\n"));
|
|
1431
1448
|
}
|
|
@@ -1772,6 +1789,70 @@ async function refreshPlotlineProgress(ctx, config, project, line) {
|
|
|
1772
1789
|
}));
|
|
1773
1790
|
return typeof raw.progress === "string" ? raw.progress.trim().slice(0, 300) : "";
|
|
1774
1791
|
}
|
|
1792
|
+
/** ✨ AI 从全书提炼角色库:大纲 + 道藏 + 编年录 + 章节摘要 → 结构化角色清单。 */
|
|
1793
|
+
async function extractRoles(ctx, config, project) {
|
|
1794
|
+
const system = [
|
|
1795
|
+
"你是一位网文角色库管理员。请根据本书的大纲、设定、编年录与章节摘要,提炼完整的角色库。",
|
|
1796
|
+
"覆盖原则:所有在编年录/章节中实际出场或有名有姓的角色都应收录;无名的功能性人物(如\"矮胖姑娘\")用其身份简称收录并标注。",
|
|
1797
|
+
"数量控制:最多输出 10 个角色,宁缺毋滥;路人级一次带过的不要收录。",
|
|
1798
|
+
"每个角色输出:",
|
|
1799
|
+
"1. name:角色名(或身份简称)。",
|
|
1800
|
+
"2. roleLabel:定位——protagonist=主角;female_lead=女主(唯一知己/感情线核心,无后宫前提下只此一位);female_support=重要女配;support=普通配角;antagonist=反派;extra=路人/背景。",
|
|
1801
|
+
"3. identity:身份一句话(宗门/势力/血脉/职业)。",
|
|
1802
|
+
"4. traits:3-6 个性格标签。",
|
|
1803
|
+
"5. goals:目标与动机一句话。",
|
|
1804
|
+
"6. relations:关系网数组,格式[\"角色名(关系)\", ...]。",
|
|
1805
|
+
"7. arc:成长线数组,格式[\"阶段:说明\", ...](如\"出场:祭品身份\"/\"转折:祭祀被中断脱身\")。",
|
|
1806
|
+
"8. knowledge:该角色已经知道的关键信息(3-8 条),不知道的信息不要写进去。",
|
|
1807
|
+
"精简要求:identity 控制在 30 字内;traits 3-6 个短标签;goals 60 字内;relations 2-5 条;arc 2-4 条;knowledge 每条 40 字内。整体输出量要紧凑,避免冗长。",
|
|
1808
|
+
"重要:用户消息里列出的「已收录角色」绝不要再次输出——这些角色已经在角色库里,跳过它们,只提炼未收录的。",
|
|
1809
|
+
"输出必须是合法 JSON 数组,不要输出其他文字:[{\"name\":\"...\", \"roleLabel\":\"...\", \"identity\":\"...\", \"traits\":[...], \"goals\":\"...\", \"relations\":[...], \"arc\":[...], \"knowledge\":[...]}]",
|
|
1810
|
+
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。",
|
|
1811
|
+
"重要:直接输出 JSON 结果本身,不要把思考过程写在输出里。"
|
|
1812
|
+
].join("\n");
|
|
1813
|
+
const written = project.chapters.filter((c) => c.status !== "pending" && c.status !== "generating");
|
|
1814
|
+
const existingRoles = project.roles ?? [];
|
|
1815
|
+
const raw = parseJsonArray(await complete(ctx, config, {
|
|
1816
|
+
system,
|
|
1817
|
+
user: [
|
|
1818
|
+
`书名:《${project.bookName}》`,
|
|
1819
|
+
existingRoles.length > 0 ? `已收录角色(跳过,不要输出):${existingRoles.map((r) => r.name).join("、")}` : "",
|
|
1820
|
+
`大纲(节选前 3000 字):\n${project.outline.slice(0, 3e3)}`,
|
|
1821
|
+
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")}` : "",
|
|
1822
|
+
(project.facts ?? []).length > 0 ? `编年录(最近 60 条):\n${(project.facts ?? []).slice(-60).map((f) => `[第${f.chapterNo}章] ${f.text.slice(0, 80)}`).join("\n")}` : "",
|
|
1823
|
+
written.length > 0 ? `已写章节标题(${written.length} 章):\n${written.map((c) => `第${c.no}章《${c.title}》`).join("、")}` : "",
|
|
1824
|
+
"只输出 JSON 数组。"
|
|
1825
|
+
].join("\n\n"),
|
|
1826
|
+
temperature: .3,
|
|
1827
|
+
maxTokens: Math.max(config.maxTokens, 24e3)
|
|
1828
|
+
}));
|
|
1829
|
+
const labels = /* @__PURE__ */ new Set([
|
|
1830
|
+
"protagonist",
|
|
1831
|
+
"female_lead",
|
|
1832
|
+
"female_support",
|
|
1833
|
+
"support",
|
|
1834
|
+
"antagonist",
|
|
1835
|
+
"extra"
|
|
1836
|
+
]);
|
|
1837
|
+
const strArr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.trim() !== "") : [];
|
|
1838
|
+
const roles = [];
|
|
1839
|
+
for (const entry of raw) {
|
|
1840
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
1841
|
+
const name = typeof entry.name === "string" ? entry.name.trim().slice(0, 30) : "";
|
|
1842
|
+
if (name === "") continue;
|
|
1843
|
+
roles.push({
|
|
1844
|
+
name,
|
|
1845
|
+
roleLabel: labels.has(entry.roleLabel) ? entry.roleLabel : "support",
|
|
1846
|
+
identity: typeof entry.identity === "string" ? entry.identity.slice(0, 100) : "",
|
|
1847
|
+
traits: strArr(entry.traits).map((t) => t.slice(0, 20)).slice(0, 8),
|
|
1848
|
+
goals: typeof entry.goals === "string" ? entry.goals.slice(0, 200) : "",
|
|
1849
|
+
relations: strArr(entry.relations).map((r) => r.slice(0, 60)).slice(0, 10),
|
|
1850
|
+
arc: strArr(entry.arc).map((a) => a.slice(0, 120)).slice(0, 10),
|
|
1851
|
+
knowledge: strArr(entry.knowledge).map((k) => k.slice(0, 120)).slice(0, 12)
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
return roles;
|
|
1855
|
+
}
|
|
1775
1856
|
/** 🩺 剧情健康检查:基于已写章节数/各线状态/编年录,判断是否需要新线及添加时机。 */
|
|
1776
1857
|
async function analyzePlotlineHealth(ctx, config, project) {
|
|
1777
1858
|
const system = [
|
|
@@ -3144,8 +3225,19 @@ async function chatOnce(ctx, config, system, history) {
|
|
|
3144
3225
|
const messages = historyToMessages(history);
|
|
3145
3226
|
const last = messages[messages.length - 1];
|
|
3146
3227
|
if (last?.role === "user" && Array.isArray(last.content)) {
|
|
3147
|
-
const
|
|
3148
|
-
|
|
3228
|
+
const blocks = last.content;
|
|
3229
|
+
const idx = blocks.findIndex((b) => b.type === "text");
|
|
3230
|
+
if (idx !== -1) {
|
|
3231
|
+
const textBlock = blocks[idx];
|
|
3232
|
+
const newBlocks = blocks.map((b, i) => i === idx ? {
|
|
3233
|
+
...textBlock,
|
|
3234
|
+
text: textBlock.text + "\n\n(回复格式提醒:如果你需要执行任何操作,你的回复必须【只包含】一个 <dsh-action name=\"工具名\">{\"参数\":值}</dsh-action> 标签,禁止先说话、禁止解释、禁止铺垫;如果你只是在回答或讨论,正常回复即可,不要输出标签。)"
|
|
3235
|
+
} : b);
|
|
3236
|
+
messages[messages.length - 1] = {
|
|
3237
|
+
...last,
|
|
3238
|
+
content: newBlocks
|
|
3239
|
+
};
|
|
3240
|
+
}
|
|
3149
3241
|
}
|
|
3150
3242
|
const request = {
|
|
3151
3243
|
provider: config.provider,
|
|
@@ -3182,9 +3274,29 @@ async function* runAssistantTurn(ctx, config, project, outputDir, userMessage) {
|
|
|
3182
3274
|
/** 已提示模型输出动作标签的次数(0 = 尚未提示;超过上限则按纯文字回复结束,防死循环)。 */
|
|
3183
3275
|
let nudged = 0;
|
|
3184
3276
|
const MAX_NUDGES = 6;
|
|
3277
|
+
/** 连续收到 hex 乱码回复的次数(≥2 次判定 LLM 侧异常,放弃本轮避免死循环)。 */
|
|
3278
|
+
let garbleCount = 0;
|
|
3185
3279
|
for (;;) {
|
|
3186
3280
|
if (round++ > 20) break;
|
|
3187
3281
|
const reply = await chatOnce(ctx, config, system, history);
|
|
3282
|
+
if (reply.length > 120 && /^[0-9a-fA-F\s]+$/.test(reply.slice(0, 2e3))) {
|
|
3283
|
+
garbleCount++;
|
|
3284
|
+
if (garbleCount >= 2) {
|
|
3285
|
+
const garbleEntry = {
|
|
3286
|
+
role: "assistant",
|
|
3287
|
+
content: "(模型本次返回了异常编码内容,已忽略;请重新描述你的问题。)",
|
|
3288
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
3289
|
+
};
|
|
3290
|
+
history.push(garbleEntry);
|
|
3291
|
+
appendHistory(outputDir, garbleEntry);
|
|
3292
|
+
yield {
|
|
3293
|
+
frame: "delta",
|
|
3294
|
+
text: garbleEntry.content
|
|
3295
|
+
};
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
continue;
|
|
3299
|
+
}
|
|
3188
3300
|
const action = extractAction(reply);
|
|
3189
3301
|
if (action === void 0) {
|
|
3190
3302
|
const intendsAction = /(改|修改|修订|重写|替换|调整|生成|新增|删除|导出|看看|查看|调出|读一下|加上|加一个|去掉|删掉|把.+改成|定位|处理|转轨|检查|确认|搜索|找一下|列一下|查一下|查一遍|查一查|再查|查查|核实|核对|清点|盘点|看一下|看下|继续)/.test(reply);
|
|
@@ -3530,6 +3642,15 @@ function makeRoutes(deps) {
|
|
|
3530
3642
|
seedBookshelfFromOutputDir(config.outputDir);
|
|
3531
3643
|
const project = loadProject(config.outputDir);
|
|
3532
3644
|
if (project !== void 0) {
|
|
3645
|
+
const staleMs = 600 * 1e3;
|
|
3646
|
+
for (const c of project.chapters) if (c.status === "generating" && c.generatingAt !== void 0) {
|
|
3647
|
+
const started = new Date(c.generatingAt).getTime();
|
|
3648
|
+
if (Number.isFinite(started) && Date.now() - started > staleMs) {
|
|
3649
|
+
c.status = "pending";
|
|
3650
|
+
c.error = void 0;
|
|
3651
|
+
c.generatingAt = void 0;
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3533
3654
|
syncProjectWithDisk(project, config.outputDir);
|
|
3534
3655
|
saveProject(config.outputDir, project);
|
|
3535
3656
|
}
|
|
@@ -3718,6 +3839,7 @@ function makeRoutes(deps) {
|
|
|
3718
3839
|
});
|
|
3719
3840
|
chapter.status = "generating";
|
|
3720
3841
|
chapter.error = void 0;
|
|
3842
|
+
chapter.generatingAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3721
3843
|
saveProject(config.outputDir, project);
|
|
3722
3844
|
const send = (frame) => {
|
|
3723
3845
|
res.write(JSON.stringify(frame) + "\n");
|
|
@@ -4233,604 +4355,700 @@ function makeRoutes(deps) {
|
|
|
4233
4355
|
writeJson(res, 200, { ok: true });
|
|
4234
4356
|
}
|
|
4235
4357
|
};
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
{
|
|
4256
|
-
kind: "exact",
|
|
4257
|
-
path: NOVEL_API.assets,
|
|
4258
|
-
handler: async (req, res) => {
|
|
4259
|
-
if (req.method !== "GET" && req.method !== "POST") {
|
|
4260
|
-
writeJson(res, 405, { error: "method not allowed (expected GET or POST)" });
|
|
4358
|
+
const assetsRoute = {
|
|
4359
|
+
kind: "exact",
|
|
4360
|
+
path: NOVEL_API.assets,
|
|
4361
|
+
handler: async (req, res) => {
|
|
4362
|
+
if (req.method !== "GET" && req.method !== "POST") {
|
|
4363
|
+
writeJson(res, 405, { error: "method not allowed (expected GET or POST)" });
|
|
4364
|
+
return;
|
|
4365
|
+
}
|
|
4366
|
+
if (!isLoopbackRequest(req)) {
|
|
4367
|
+
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
4368
|
+
return;
|
|
4369
|
+
}
|
|
4370
|
+
const config = getConfig();
|
|
4371
|
+
const project = loadProject(config.outputDir);
|
|
4372
|
+
const projectAssets = project?.assets ?? emptyProjectAssets();
|
|
4373
|
+
if (req.method === "POST") {
|
|
4374
|
+
const body = await readJsonBody(req);
|
|
4375
|
+
if (body === void 0) {
|
|
4376
|
+
writeJson(res, 400, { error: "无效的 JSON" });
|
|
4261
4377
|
return;
|
|
4262
4378
|
}
|
|
4263
|
-
if (
|
|
4264
|
-
writeJson(res,
|
|
4379
|
+
if (project === void 0) {
|
|
4380
|
+
writeJson(res, 400, { error: "请先加载大纲创建项目" });
|
|
4265
4381
|
return;
|
|
4266
4382
|
}
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
if (
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4383
|
+
if (body.genre !== void 0) projectAssets.genre = body.genre;
|
|
4384
|
+
if (body.primaryProgression !== void 0) projectAssets.primaryProgression = body.primaryProgression;
|
|
4385
|
+
if (body.auxiliaryProgressions !== void 0) projectAssets.auxiliaryProgressions = body.auxiliaryProgressions;
|
|
4386
|
+
if (body.antiAiRules !== void 0) projectAssets.antiAiRules = body.antiAiRules;
|
|
4387
|
+
if (body.styleAssets !== void 0) projectAssets.styleAssets = body.styleAssets;
|
|
4388
|
+
projectAssets.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4389
|
+
project.assets = projectAssets;
|
|
4390
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4391
|
+
saveProject(config.outputDir, project);
|
|
4392
|
+
}
|
|
4393
|
+
writeJson(res, 200, {
|
|
4394
|
+
projectAssets,
|
|
4395
|
+
genreLibrary: BUILTIN_GENRE_LIBRARY,
|
|
4396
|
+
antiAiLibrary: BUILTIN_ANTI_AI_RULES,
|
|
4397
|
+
styleTemplates: BUILTIN_STYLE_TEMPLATES,
|
|
4398
|
+
progressionLibrary: BUILTIN_PROGRESSION_MODES
|
|
4399
|
+
});
|
|
4400
|
+
}
|
|
4401
|
+
};
|
|
4402
|
+
const styleEngineRoute = {
|
|
4403
|
+
kind: "exact",
|
|
4404
|
+
path: NOVEL_API.styleEngine,
|
|
4405
|
+
handler: async (req, res) => {
|
|
4406
|
+
if (!guard(req, res, "POST")) return;
|
|
4407
|
+
const config = getConfig();
|
|
4408
|
+
const project = loadProject(config.outputDir);
|
|
4409
|
+
const body = await readJsonBody(req);
|
|
4410
|
+
const sample = body?.sampleText?.trim();
|
|
4411
|
+
if (sample === void 0 || sample.length < 50) {
|
|
4412
|
+
writeJson(res, 400, { error: "样本文本过短(<50 字符),请粘贴一段能代表目标风格的文字" });
|
|
4413
|
+
return;
|
|
4414
|
+
}
|
|
4415
|
+
try {
|
|
4416
|
+
const rules = await extractStyleAsset(ctx, config, sample);
|
|
4417
|
+
const styleAsset = {
|
|
4418
|
+
name: (body?.name?.trim() !== "" && body?.name !== void 0 ? body.name : `风格资产 ${Date.now().toString(36)}`).slice(0, 40),
|
|
4419
|
+
...rules,
|
|
4420
|
+
sourceText: sample.slice(0, 3e3),
|
|
4421
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4422
|
+
};
|
|
4423
|
+
if (project !== void 0) {
|
|
4424
|
+
project.assets ??= emptyProjectAssets();
|
|
4425
|
+
project.assets.styleAssets ??= [];
|
|
4426
|
+
project.assets.styleAssets.push(styleAsset);
|
|
4427
|
+
project.assets.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4287
4428
|
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4288
4429
|
saveProject(config.outputDir, project);
|
|
4289
4430
|
}
|
|
4290
|
-
writeJson(res, 200, {
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
antiAiLibrary: BUILTIN_ANTI_AI_RULES,
|
|
4294
|
-
styleTemplates: BUILTIN_STYLE_TEMPLATES,
|
|
4295
|
-
progressionLibrary: BUILTIN_PROGRESSION_MODES
|
|
4296
|
-
});
|
|
4431
|
+
writeJson(res, 200, { styleAsset });
|
|
4432
|
+
} catch (error) {
|
|
4433
|
+
writeJson(res, 500, { error: error.message });
|
|
4297
4434
|
}
|
|
4298
|
-
}
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
const sample = body?.sampleText?.trim();
|
|
4308
|
-
if (sample === void 0 || sample.length < 50) {
|
|
4309
|
-
writeJson(res, 400, { error: "样本文本过短(<50 字符),请粘贴一段能代表目标风格的文字" });
|
|
4435
|
+
}
|
|
4436
|
+
};
|
|
4437
|
+
const bookshelfRoute = {
|
|
4438
|
+
kind: "exact",
|
|
4439
|
+
path: NOVEL_API.bookshelf,
|
|
4440
|
+
handler: async (req, res) => {
|
|
4441
|
+
if (req.method === "GET") {
|
|
4442
|
+
if (!isLoopbackRequest(req)) {
|
|
4443
|
+
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
4310
4444
|
return;
|
|
4311
4445
|
}
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
name: (body?.name?.trim() !== "" && body?.name !== void 0 ? body.name : `风格资产 ${Date.now().toString(36)}`).slice(0, 40),
|
|
4316
|
-
...rules,
|
|
4317
|
-
sourceText: sample.slice(0, 3e3),
|
|
4318
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4319
|
-
};
|
|
4320
|
-
if (project !== void 0) {
|
|
4321
|
-
project.assets ??= emptyProjectAssets();
|
|
4322
|
-
project.assets.styleAssets ??= [];
|
|
4323
|
-
project.assets.styleAssets.push(styleAsset);
|
|
4324
|
-
project.assets.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4325
|
-
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4326
|
-
saveProject(config.outputDir, project);
|
|
4327
|
-
}
|
|
4328
|
-
writeJson(res, 200, { styleAsset });
|
|
4329
|
-
} catch (error) {
|
|
4330
|
-
writeJson(res, 500, { error: error.message });
|
|
4331
|
-
}
|
|
4446
|
+
seedBookshelfFromOutputDir(getConfig().outputDir);
|
|
4447
|
+
writeJson(res, 200, bookshelfSnapshot(loadBookshelf()));
|
|
4448
|
+
return;
|
|
4332
4449
|
}
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
assistantClearRoute,
|
|
4337
|
-
{
|
|
4338
|
-
kind: "exact",
|
|
4339
|
-
path: NOVEL_API.bookshelf,
|
|
4340
|
-
handler: async (req, res) => {
|
|
4341
|
-
if (req.method === "GET") {
|
|
4342
|
-
if (!isLoopbackRequest(req)) {
|
|
4343
|
-
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
4344
|
-
return;
|
|
4345
|
-
}
|
|
4346
|
-
seedBookshelfFromOutputDir(getConfig().outputDir);
|
|
4347
|
-
writeJson(res, 200, bookshelfSnapshot(loadBookshelf()));
|
|
4348
|
-
return;
|
|
4349
|
-
}
|
|
4350
|
-
if (req.method === "POST") {
|
|
4351
|
-
if (!isLoopbackRequest(req)) {
|
|
4352
|
-
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
4353
|
-
return;
|
|
4354
|
-
}
|
|
4355
|
-
const body = await readJsonBody(req);
|
|
4356
|
-
const bookName = body?.bookName?.trim();
|
|
4357
|
-
if (bookName === void 0 || bookName === "") {
|
|
4358
|
-
writeJson(res, 400, { error: "bookName 不能为空" });
|
|
4359
|
-
return;
|
|
4360
|
-
}
|
|
4361
|
-
const outputDir = body?.outputDir?.trim() !== "" && body?.outputDir !== void 0 ? body.outputDir : defaultOutputDirFor(bookName);
|
|
4362
|
-
const book = createBook(bookName, outputDir);
|
|
4363
|
-
const outline = body?.outline?.trim();
|
|
4364
|
-
if (outline !== void 0 && outline.length >= 50) {
|
|
4365
|
-
const project = createProject(outline);
|
|
4366
|
-
saveProject(outputDir, project);
|
|
4367
|
-
renameBook(book.id, project.bookName);
|
|
4368
|
-
}
|
|
4369
|
-
writeJson(res, 200, bookshelfSnapshot(loadBookshelf()));
|
|
4450
|
+
if (req.method === "POST") {
|
|
4451
|
+
if (!isLoopbackRequest(req)) {
|
|
4452
|
+
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
4370
4453
|
return;
|
|
4371
4454
|
}
|
|
4372
|
-
writeJson(res, 405, { error: "method not allowed (expected GET or POST)" });
|
|
4373
|
-
}
|
|
4374
|
-
},
|
|
4375
|
-
{
|
|
4376
|
-
kind: "exact",
|
|
4377
|
-
path: "/api/dsh-novel-forge/bookshelf/activate",
|
|
4378
|
-
handler: async (req, res) => {
|
|
4379
|
-
if (!guard(req, res, "POST")) return;
|
|
4380
4455
|
const body = await readJsonBody(req);
|
|
4381
|
-
|
|
4382
|
-
|
|
4456
|
+
const bookName = body?.bookName?.trim();
|
|
4457
|
+
if (bookName === void 0 || bookName === "") {
|
|
4458
|
+
writeJson(res, 400, { error: "bookName 不能为空" });
|
|
4383
4459
|
return;
|
|
4384
4460
|
}
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4461
|
+
const outputDir = body?.outputDir?.trim() !== "" && body?.outputDir !== void 0 ? body.outputDir : defaultOutputDirFor(bookName);
|
|
4462
|
+
const book = createBook(bookName, outputDir);
|
|
4463
|
+
const outline = body?.outline?.trim();
|
|
4464
|
+
if (outline !== void 0 && outline.length >= 50) {
|
|
4465
|
+
const project = createProject(outline);
|
|
4466
|
+
saveProject(outputDir, project);
|
|
4467
|
+
renameBook(book.id, project.bookName);
|
|
4388
4468
|
}
|
|
4389
4469
|
writeJson(res, 200, bookshelfSnapshot(loadBookshelf()));
|
|
4470
|
+
return;
|
|
4390
4471
|
}
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
}
|
|
4406
|
-
writeJson(res, 200, bookshelfSnapshot(loadBookshelf()));
|
|
4472
|
+
writeJson(res, 405, { error: "method not allowed (expected GET or POST)" });
|
|
4473
|
+
}
|
|
4474
|
+
};
|
|
4475
|
+
/** 重置项目:清空设定/卷/章节计划/正文/伏笔/资产/事实库(可携带新大纲)。 */
|
|
4476
|
+
const resetRoute = {
|
|
4477
|
+
kind: "exact",
|
|
4478
|
+
path: NOVEL_API.reset,
|
|
4479
|
+
handler: async (req, res) => {
|
|
4480
|
+
if (!guard(req, res, "POST")) return;
|
|
4481
|
+
const config = getConfig();
|
|
4482
|
+
const project = loadProject(config.outputDir);
|
|
4483
|
+
if (project === void 0) {
|
|
4484
|
+
writeJson(res, 400, { error: "输出目录中没有项目,无需重置" });
|
|
4485
|
+
return;
|
|
4407
4486
|
}
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4487
|
+
const outline = (await readJsonBody(req))?.outline?.trim();
|
|
4488
|
+
if (outline !== void 0 && outline.length >= 50) {
|
|
4489
|
+
project.outline = outline;
|
|
4490
|
+
project.bookName = createProject(outline).bookName;
|
|
4491
|
+
}
|
|
4492
|
+
project.bible = void 0;
|
|
4493
|
+
project.volumes = void 0;
|
|
4494
|
+
project.chapters = [];
|
|
4495
|
+
project.foreshadows = [];
|
|
4496
|
+
project.assets = emptyProjectAssets();
|
|
4497
|
+
project.facts = [];
|
|
4498
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4499
|
+
saveProject(config.outputDir, project);
|
|
4500
|
+
writeJson(res, 200, {
|
|
4501
|
+
ok: true,
|
|
4502
|
+
bookName: project.bookName
|
|
4503
|
+
});
|
|
4504
|
+
}
|
|
4505
|
+
};
|
|
4506
|
+
const bookshelfActivateRoute = {
|
|
4507
|
+
kind: "exact",
|
|
4508
|
+
path: "/api/dsh-novel-forge/bookshelf/activate",
|
|
4509
|
+
handler: async (req, res) => {
|
|
4510
|
+
if (!guard(req, res, "POST")) return;
|
|
4511
|
+
const body = await readJsonBody(req);
|
|
4512
|
+
if (body?.id === void 0 || body.id === "") {
|
|
4513
|
+
writeJson(res, 400, { error: "id 不能为空" });
|
|
4514
|
+
return;
|
|
4515
|
+
}
|
|
4516
|
+
if (activateBook(body.id) === void 0) {
|
|
4517
|
+
writeJson(res, 404, { error: `书 ${body.id} 不存在` });
|
|
4518
|
+
return;
|
|
4519
|
+
}
|
|
4520
|
+
writeJson(res, 200, bookshelfSnapshot(loadBookshelf()));
|
|
4521
|
+
}
|
|
4522
|
+
};
|
|
4523
|
+
const bookshelfRemoveRoute = {
|
|
4524
|
+
kind: "exact",
|
|
4525
|
+
path: "/api/dsh-novel-forge/bookshelf/remove",
|
|
4526
|
+
handler: async (req, res) => {
|
|
4527
|
+
if (!guard(req, res, "POST")) return;
|
|
4528
|
+
const body = await readJsonBody(req);
|
|
4529
|
+
if (body?.id === void 0 || body.id === "") {
|
|
4530
|
+
writeJson(res, 400, { error: "id 不能为空" });
|
|
4531
|
+
return;
|
|
4532
|
+
}
|
|
4533
|
+
if (!removeBook(body.id)) {
|
|
4534
|
+
writeJson(res, 404, { error: `书 ${body.id} 不存在` });
|
|
4535
|
+
return;
|
|
4536
|
+
}
|
|
4537
|
+
writeJson(res, 200, bookshelfSnapshot(loadBookshelf()));
|
|
4538
|
+
}
|
|
4539
|
+
};
|
|
4540
|
+
/** 全书一致性质检。 */
|
|
4541
|
+
const auditRoute = {
|
|
4542
|
+
kind: "exact",
|
|
4543
|
+
path: NOVEL_API.audit,
|
|
4544
|
+
handler: async (req, res) => {
|
|
4545
|
+
if (!guard(req, res, "POST")) return;
|
|
4546
|
+
const config = getConfig();
|
|
4547
|
+
const project = requireProject(res);
|
|
4548
|
+
if (project === void 0) return;
|
|
4549
|
+
try {
|
|
4550
|
+
writeJson(res, 200, {
|
|
4551
|
+
issues: await auditBook(ctx, config, project, config.outputDir),
|
|
4552
|
+
auditedChapters: project.chapters.filter((c) => c.status !== "pending" && c.status !== "generating").length,
|
|
4553
|
+
auditedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4554
|
+
});
|
|
4555
|
+
} catch (error) {
|
|
4556
|
+
writeJson(res, 500, { error: error.message });
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
};
|
|
4560
|
+
/** 角色卡刷新(出场统计精确化 + LLM 聚合状态)。 */
|
|
4561
|
+
const charactersRefreshRoute = {
|
|
4562
|
+
kind: "exact",
|
|
4563
|
+
path: NOVEL_API.charactersRefresh,
|
|
4564
|
+
handler: async (req, res) => {
|
|
4565
|
+
if (!guard(req, res, "POST")) return;
|
|
4566
|
+
const config = getConfig();
|
|
4567
|
+
const project = requireProject(res);
|
|
4568
|
+
if (project === void 0) return;
|
|
4569
|
+
try {
|
|
4570
|
+
const cards = await refreshCharacters(ctx, config, project, config.outputDir);
|
|
4571
|
+
project.roleStatus = cards;
|
|
4431
4572
|
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4432
4573
|
saveProject(config.outputDir, project);
|
|
4574
|
+
writeJson(res, 200, { cards });
|
|
4575
|
+
} catch (error) {
|
|
4576
|
+
writeJson(res, 500, { error: error.message });
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
};
|
|
4580
|
+
/** 事实库回填:对历史已生成章节批量抽取事实。 */
|
|
4581
|
+
const factsBackfillRoute = {
|
|
4582
|
+
kind: "exact",
|
|
4583
|
+
path: NOVEL_API.factsBackfill,
|
|
4584
|
+
handler: async (req, res) => {
|
|
4585
|
+
if (!guard(req, res, "POST")) return;
|
|
4586
|
+
const config = getConfig();
|
|
4587
|
+
const project = requireProject(res);
|
|
4588
|
+
if (project === void 0) return;
|
|
4589
|
+
try {
|
|
4433
4590
|
writeJson(res, 200, {
|
|
4434
4591
|
ok: true,
|
|
4435
|
-
|
|
4592
|
+
filled: await backfillFacts(ctx, config, project, config.outputDir)
|
|
4436
4593
|
});
|
|
4594
|
+
} catch (error) {
|
|
4595
|
+
writeJson(res, 500, { error: error.message });
|
|
4437
4596
|
}
|
|
4438
|
-
}
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
});
|
|
4453
|
-
} catch (error) {
|
|
4454
|
-
writeJson(res, 500, { error: error.message });
|
|
4455
|
-
}
|
|
4597
|
+
}
|
|
4598
|
+
};
|
|
4599
|
+
/** 设定圣经局部修补(世界观规则/红线/风格)。 */
|
|
4600
|
+
const biblePatchRoute = {
|
|
4601
|
+
kind: "exact",
|
|
4602
|
+
path: NOVEL_API.biblePatch,
|
|
4603
|
+
handler: async (req, res) => {
|
|
4604
|
+
if (!guard(req, res, "POST")) return;
|
|
4605
|
+
const config = getConfig();
|
|
4606
|
+
const project = requireProject(res);
|
|
4607
|
+
if (project === void 0) return;
|
|
4608
|
+
if (project.bible === void 0) {
|
|
4609
|
+
writeJson(res, 400, { error: "尚未生成道藏,请先生成" });
|
|
4610
|
+
return;
|
|
4456
4611
|
}
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4612
|
+
const body = await readJsonBody(req);
|
|
4613
|
+
if (Array.isArray(body?.worldRules)) project.bible.worldRules = body.worldRules.filter((r) => r.trim() !== "");
|
|
4614
|
+
if (Array.isArray(body?.redLines)) project.bible.redLines = body.redLines.filter((r) => r.trim() !== "");
|
|
4615
|
+
if (Array.isArray(body?.style)) project.bible.style = body.style.filter((r) => r.trim() !== "");
|
|
4616
|
+
if (Array.isArray(body?.characters)) project.bible.characters = body.characters.filter((c) => c !== void 0 && c !== null && typeof c.name === "string" && c.name.trim() !== "").map((c) => ({
|
|
4617
|
+
name: c.name.trim(),
|
|
4618
|
+
role: [
|
|
4619
|
+
"protagonist",
|
|
4620
|
+
"supporting",
|
|
4621
|
+
"antagonist",
|
|
4622
|
+
"other"
|
|
4623
|
+
].includes(c.role) ? c.role : "other",
|
|
4624
|
+
traits: Array.isArray(c.traits) ? c.traits.filter((t) => typeof t === "string" && t.trim() !== "") : [],
|
|
4625
|
+
goals: typeof c.goals === "string" ? c.goals : "",
|
|
4626
|
+
relations: typeof c.relations === "string" ? c.relations : "",
|
|
4627
|
+
knowledge: Array.isArray(c.knowledge) ? c.knowledge.filter((k) => typeof k === "string" && k.trim() !== "") : void 0
|
|
4628
|
+
}));
|
|
4629
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4630
|
+
saveProject(config.outputDir, project);
|
|
4631
|
+
writeJson(res, 200, { bible: project.bible });
|
|
4632
|
+
}
|
|
4633
|
+
};
|
|
4634
|
+
/** 小说简介:AI 生成/补全,或手动保存。 */
|
|
4635
|
+
const blurbRoute = {
|
|
4636
|
+
kind: "exact",
|
|
4637
|
+
path: NOVEL_API.blurb,
|
|
4638
|
+
handler: async (req, res) => {
|
|
4639
|
+
if (!guard(req, res, "POST")) return;
|
|
4640
|
+
const config = getConfig();
|
|
4641
|
+
const project = requireProject(res);
|
|
4642
|
+
if (project === void 0) return;
|
|
4643
|
+
const body = await readJsonBody(req);
|
|
4644
|
+
try {
|
|
4645
|
+
if (body?.action === "save") {
|
|
4646
|
+
const text = body.text?.trim() ?? "";
|
|
4647
|
+
project.blurb = text;
|
|
4648
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4649
|
+
saveProject(config.outputDir, project);
|
|
4650
|
+
writeJson(res, 200, { blurb: text });
|
|
4651
|
+
return;
|
|
4470
4652
|
}
|
|
4653
|
+
const partial = body?.partial?.trim() ?? "";
|
|
4654
|
+
const blurb = await generateBlurb(ctx, config, project, partial);
|
|
4655
|
+
project.blurb = blurb;
|
|
4656
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4657
|
+
saveProject(config.outputDir, project);
|
|
4658
|
+
writeJson(res, 200, { blurb });
|
|
4659
|
+
} catch (error) {
|
|
4660
|
+
writeJson(res, 500, { error: error.message });
|
|
4471
4661
|
}
|
|
4472
|
-
}
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
writeJson(res,
|
|
4483
|
-
|
|
4484
|
-
filled: await backfillFacts(ctx, config, project, config.outputDir)
|
|
4485
|
-
});
|
|
4486
|
-
} catch (error) {
|
|
4487
|
-
writeJson(res, 500, { error: error.message });
|
|
4662
|
+
}
|
|
4663
|
+
};
|
|
4664
|
+
/** 封面:GET 读取(dataUrl);POST 上传(base64)或移除。 */
|
|
4665
|
+
const coverRoute = {
|
|
4666
|
+
kind: "exact",
|
|
4667
|
+
path: NOVEL_API.cover,
|
|
4668
|
+
handler: async (req, res) => {
|
|
4669
|
+
const config = getConfig();
|
|
4670
|
+
if (req.method === "GET") {
|
|
4671
|
+
if (!isLoopbackRequest(req)) {
|
|
4672
|
+
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
4673
|
+
return;
|
|
4488
4674
|
}
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
handler: async (req, res) => {
|
|
4495
|
-
if (!guard(req, res, "POST")) return;
|
|
4496
|
-
const config = getConfig();
|
|
4497
|
-
const project = requireProject(res);
|
|
4498
|
-
if (project === void 0) return;
|
|
4499
|
-
if (project.bible === void 0) {
|
|
4500
|
-
writeJson(res, 400, { error: "尚未生成道藏,请先生成" });
|
|
4675
|
+
const dirParam = new URL(req.url ?? "/", "http://localhost").searchParams.get("dir");
|
|
4676
|
+
const targetDir = dirParam !== null && dirParam !== "" ? dirParam : config.outputDir;
|
|
4677
|
+
const coverPath = loadProject(targetDir)?.coverPath;
|
|
4678
|
+
if (coverPath === void 0 || coverPath === "") {
|
|
4679
|
+
writeJson(res, 200, { dataUrl: null });
|
|
4501
4680
|
return;
|
|
4502
4681
|
}
|
|
4503
|
-
const
|
|
4504
|
-
if (
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
"protagonist",
|
|
4511
|
-
"supporting",
|
|
4512
|
-
"antagonist",
|
|
4513
|
-
"other"
|
|
4514
|
-
].includes(c.role) ? c.role : "other",
|
|
4515
|
-
traits: Array.isArray(c.traits) ? c.traits.filter((t) => typeof t === "string" && t.trim() !== "") : [],
|
|
4516
|
-
goals: typeof c.goals === "string" ? c.goals : "",
|
|
4517
|
-
relations: typeof c.relations === "string" ? c.relations : "",
|
|
4518
|
-
knowledge: Array.isArray(c.knowledge) ? c.knowledge.filter((k) => typeof k === "string" && k.trim() !== "") : void 0
|
|
4519
|
-
}));
|
|
4520
|
-
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4521
|
-
saveProject(config.outputDir, project);
|
|
4522
|
-
writeJson(res, 200, { bible: project.bible });
|
|
4682
|
+
const file = join(targetDir, coverPath);
|
|
4683
|
+
if (!existsSync(file)) {
|
|
4684
|
+
writeJson(res, 200, { dataUrl: null });
|
|
4685
|
+
return;
|
|
4686
|
+
}
|
|
4687
|
+
writeJson(res, 200, { dataUrl: `data:${coverPath.toLowerCase().endsWith(".png") ? "image/png" : coverPath.toLowerCase().endsWith(".jpg") || coverPath.toLowerCase().endsWith(".jpeg") ? "image/jpeg" : coverPath.toLowerCase().endsWith(".webp") ? "image/webp" : "image/png"};base64,${readFileSync(file).toString("base64")}` });
|
|
4688
|
+
return;
|
|
4523
4689
|
}
|
|
4524
|
-
|
|
4525
|
-
{
|
|
4526
|
-
kind: "exact",
|
|
4527
|
-
path: NOVEL_API.blurb,
|
|
4528
|
-
handler: async (req, res) => {
|
|
4690
|
+
if (req.method === "POST") {
|
|
4529
4691
|
if (!guard(req, res, "POST")) return;
|
|
4530
|
-
const config = getConfig();
|
|
4531
4692
|
const project = requireProject(res);
|
|
4532
4693
|
if (project === void 0) return;
|
|
4533
4694
|
const body = await readJsonBody(req);
|
|
4534
4695
|
try {
|
|
4535
|
-
if (body?.action === "
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4539
|
-
saveProject(config.outputDir, project);
|
|
4540
|
-
writeJson(res, 200, { blurb: text });
|
|
4541
|
-
return;
|
|
4542
|
-
}
|
|
4543
|
-
const partial = body?.partial?.trim() ?? "";
|
|
4544
|
-
const blurb = await generateBlurb(ctx, config, project, partial);
|
|
4545
|
-
project.blurb = blurb;
|
|
4546
|
-
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4547
|
-
saveProject(config.outputDir, project);
|
|
4548
|
-
writeJson(res, 200, { blurb });
|
|
4549
|
-
} catch (error) {
|
|
4550
|
-
writeJson(res, 500, { error: error.message });
|
|
4551
|
-
}
|
|
4552
|
-
}
|
|
4553
|
-
},
|
|
4554
|
-
{
|
|
4555
|
-
kind: "exact",
|
|
4556
|
-
path: NOVEL_API.cover,
|
|
4557
|
-
handler: async (req, res) => {
|
|
4558
|
-
const config = getConfig();
|
|
4559
|
-
if (req.method === "GET") {
|
|
4560
|
-
if (!isLoopbackRequest(req)) {
|
|
4561
|
-
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
4562
|
-
return;
|
|
4563
|
-
}
|
|
4564
|
-
const dirParam = new URL(req.url ?? "/", "http://localhost").searchParams.get("dir");
|
|
4565
|
-
const targetDir = dirParam !== null && dirParam !== "" ? dirParam : config.outputDir;
|
|
4566
|
-
const coverPath = loadProject(targetDir)?.coverPath;
|
|
4567
|
-
if (coverPath === void 0 || coverPath === "") {
|
|
4568
|
-
writeJson(res, 200, { dataUrl: null });
|
|
4569
|
-
return;
|
|
4570
|
-
}
|
|
4571
|
-
const file = join(targetDir, coverPath);
|
|
4572
|
-
if (!existsSync(file)) {
|
|
4573
|
-
writeJson(res, 200, { dataUrl: null });
|
|
4574
|
-
return;
|
|
4575
|
-
}
|
|
4576
|
-
writeJson(res, 200, { dataUrl: `data:${coverPath.toLowerCase().endsWith(".png") ? "image/png" : coverPath.toLowerCase().endsWith(".jpg") || coverPath.toLowerCase().endsWith(".jpeg") ? "image/jpeg" : coverPath.toLowerCase().endsWith(".webp") ? "image/webp" : "image/png"};base64,${readFileSync(file).toString("base64")}` });
|
|
4577
|
-
return;
|
|
4578
|
-
}
|
|
4579
|
-
if (req.method === "POST") {
|
|
4580
|
-
if (!guard(req, res, "POST")) return;
|
|
4581
|
-
const project = requireProject(res);
|
|
4582
|
-
if (project === void 0) return;
|
|
4583
|
-
const body = await readJsonBody(req);
|
|
4584
|
-
try {
|
|
4585
|
-
if (body?.action === "remove") {
|
|
4586
|
-
if (project.coverPath !== void 0 && project.coverPath !== "") {
|
|
4587
|
-
const oldFile = join(config.outputDir, project.coverPath);
|
|
4588
|
-
if (existsSync(oldFile)) rmSync(oldFile, { force: true });
|
|
4589
|
-
}
|
|
4590
|
-
project.coverPath = void 0;
|
|
4591
|
-
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4592
|
-
saveProject(config.outputDir, project);
|
|
4593
|
-
writeJson(res, 200, {
|
|
4594
|
-
ok: true,
|
|
4595
|
-
coverPath: null
|
|
4596
|
-
});
|
|
4597
|
-
return;
|
|
4598
|
-
}
|
|
4599
|
-
const dataUrl = body?.dataUrl ?? "";
|
|
4600
|
-
const match = /^data:(image\/(?:png|jpeg|jpg|webp));base64,(.+)$/s.exec(dataUrl);
|
|
4601
|
-
if (match === null) {
|
|
4602
|
-
writeJson(res, 400, { error: "封面须为 PNG/JPEG/WebP 的 base64 data URL" });
|
|
4603
|
-
return;
|
|
4604
|
-
}
|
|
4605
|
-
const mime = match[1];
|
|
4606
|
-
const fileName = `cover.${mime === "image/png" ? "png" : mime === "image/webp" ? "webp" : "jpg"}`;
|
|
4607
|
-
const targetPath = join(config.outputDir, fileName);
|
|
4608
|
-
const oldPath = project.coverPath;
|
|
4609
|
-
mkdirSync(config.outputDir, { recursive: true });
|
|
4610
|
-
writeFileSync(targetPath, Buffer.from(match[2], "base64"));
|
|
4611
|
-
if (oldPath !== void 0 && oldPath !== "" && oldPath !== fileName) {
|
|
4612
|
-
const oldFile = join(config.outputDir, oldPath);
|
|
4696
|
+
if (body?.action === "remove") {
|
|
4697
|
+
if (project.coverPath !== void 0 && project.coverPath !== "") {
|
|
4698
|
+
const oldFile = join(config.outputDir, project.coverPath);
|
|
4613
4699
|
if (existsSync(oldFile)) rmSync(oldFile, { force: true });
|
|
4614
4700
|
}
|
|
4615
|
-
project.coverPath =
|
|
4701
|
+
project.coverPath = void 0;
|
|
4616
4702
|
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4617
4703
|
saveProject(config.outputDir, project);
|
|
4618
4704
|
writeJson(res, 200, {
|
|
4619
4705
|
ok: true,
|
|
4620
|
-
coverPath:
|
|
4706
|
+
coverPath: null
|
|
4621
4707
|
});
|
|
4622
|
-
|
|
4623
|
-
writeJson(res, 500, { error: error.message });
|
|
4708
|
+
return;
|
|
4624
4709
|
}
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
},
|
|
4630
|
-
{
|
|
4631
|
-
kind: "exact",
|
|
4632
|
-
path: NOVEL_API.world,
|
|
4633
|
-
handler: async (req, res) => {
|
|
4634
|
-
if (!guard(req, res, "POST")) return;
|
|
4635
|
-
const config = getConfig();
|
|
4636
|
-
const project = requireProject(res);
|
|
4637
|
-
if (project === void 0) return;
|
|
4638
|
-
const body = await readJsonBody(req);
|
|
4639
|
-
try {
|
|
4640
|
-
if (body?.action === "save" && body.world !== void 0) {
|
|
4641
|
-
project.world = {
|
|
4642
|
-
realms: Array.isArray(body.world.realms) ? body.world.realms : [],
|
|
4643
|
-
regions: Array.isArray(body.world.regions) ? body.world.regions : [],
|
|
4644
|
-
factions: Array.isArray(body.world.factions) ? body.world.factions : []
|
|
4645
|
-
};
|
|
4646
|
-
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4647
|
-
saveProject(config.outputDir, project);
|
|
4648
|
-
writeJson(res, 200, { world: project.world });
|
|
4710
|
+
const dataUrl = body?.dataUrl ?? "";
|
|
4711
|
+
const match = /^data:(image\/(?:png|jpeg|jpg|webp));base64,(.+)$/s.exec(dataUrl);
|
|
4712
|
+
if (match === null) {
|
|
4713
|
+
writeJson(res, 400, { error: "封面须为 PNG/JPEG/WebP 的 base64 data URL" });
|
|
4649
4714
|
return;
|
|
4650
4715
|
}
|
|
4651
|
-
const
|
|
4652
|
-
|
|
4716
|
+
const mime = match[1];
|
|
4717
|
+
const fileName = `cover.${mime === "image/png" ? "png" : mime === "image/webp" ? "webp" : "jpg"}`;
|
|
4718
|
+
const targetPath = join(config.outputDir, fileName);
|
|
4719
|
+
const oldPath = project.coverPath;
|
|
4720
|
+
mkdirSync(config.outputDir, { recursive: true });
|
|
4721
|
+
writeFileSync(targetPath, Buffer.from(match[2], "base64"));
|
|
4722
|
+
if (oldPath !== void 0 && oldPath !== "" && oldPath !== fileName) {
|
|
4723
|
+
const oldFile = join(config.outputDir, oldPath);
|
|
4724
|
+
if (existsSync(oldFile)) rmSync(oldFile, { force: true });
|
|
4725
|
+
}
|
|
4726
|
+
project.coverPath = fileName;
|
|
4653
4727
|
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4654
4728
|
saveProject(config.outputDir, project);
|
|
4655
|
-
writeJson(res, 200, {
|
|
4729
|
+
writeJson(res, 200, {
|
|
4730
|
+
ok: true,
|
|
4731
|
+
coverPath: fileName
|
|
4732
|
+
});
|
|
4656
4733
|
} catch (error) {
|
|
4657
4734
|
writeJson(res, 500, { error: error.message });
|
|
4658
4735
|
}
|
|
4736
|
+
return;
|
|
4659
4737
|
}
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4738
|
+
writeJson(res, 405, { error: "method not allowed (expected GET or POST)" });
|
|
4739
|
+
}
|
|
4740
|
+
};
|
|
4741
|
+
/** 大世界:AI 提炼或手动保存(境界体系/区域/势力)。 */
|
|
4742
|
+
const worldRoute = {
|
|
4743
|
+
kind: "exact",
|
|
4744
|
+
path: NOVEL_API.world,
|
|
4745
|
+
handler: async (req, res) => {
|
|
4746
|
+
if (!guard(req, res, "POST")) return;
|
|
4747
|
+
const config = getConfig();
|
|
4748
|
+
const project = requireProject(res);
|
|
4749
|
+
if (project === void 0) return;
|
|
4750
|
+
const body = await readJsonBody(req);
|
|
4751
|
+
try {
|
|
4752
|
+
if (body?.action === "save" && body.world !== void 0) {
|
|
4753
|
+
project.world = {
|
|
4754
|
+
realms: Array.isArray(body.world.realms) ? body.world.realms : [],
|
|
4755
|
+
regions: Array.isArray(body.world.regions) ? body.world.regions : [],
|
|
4756
|
+
factions: Array.isArray(body.world.factions) ? body.world.factions : []
|
|
4757
|
+
};
|
|
4758
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4759
|
+
saveProject(config.outputDir, project);
|
|
4760
|
+
writeJson(res, 200, { world: project.world });
|
|
4672
4761
|
return;
|
|
4673
4762
|
}
|
|
4674
|
-
|
|
4763
|
+
const world = await extractWorld(ctx, config, project);
|
|
4764
|
+
project.world = world;
|
|
4675
4765
|
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4676
4766
|
saveProject(config.outputDir, project);
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
writeJson(res,
|
|
4767
|
+
writeJson(res, 200, { world });
|
|
4768
|
+
} catch (error) {
|
|
4769
|
+
writeJson(res, 500, { error: error.message });
|
|
4680
4770
|
}
|
|
4681
|
-
}
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4771
|
+
}
|
|
4772
|
+
};
|
|
4773
|
+
/** 重命名当前书:同步项目 bookName 与书架条目。 */
|
|
4774
|
+
const renameRoute = {
|
|
4775
|
+
kind: "exact",
|
|
4776
|
+
path: NOVEL_API.rename,
|
|
4777
|
+
handler: async (req, res) => {
|
|
4778
|
+
if (!guard(req, res, "POST")) return;
|
|
4779
|
+
const config = getConfig();
|
|
4780
|
+
const project = requireProject(res);
|
|
4781
|
+
if (project === void 0) return;
|
|
4782
|
+
const bookName = (await readJsonBody(req))?.bookName?.trim();
|
|
4783
|
+
if (bookName === void 0 || bookName === "") {
|
|
4784
|
+
writeJson(res, 400, { error: "书名不能为空" });
|
|
4785
|
+
return;
|
|
4786
|
+
}
|
|
4787
|
+
project.bookName = bookName.slice(0, 60);
|
|
4788
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4789
|
+
saveProject(config.outputDir, project);
|
|
4790
|
+
const store = loadBookshelf();
|
|
4791
|
+
if (store.activeBookId !== null) renameBook(store.activeBookId, project.bookName);
|
|
4792
|
+
writeJson(res, 200, { bookName: project.bookName });
|
|
4793
|
+
}
|
|
4794
|
+
};
|
|
4795
|
+
/** 剧情线管理:增删改 + 关联章节(主线/支线/人物线/悬念线)。 */
|
|
4796
|
+
const plotlinesRoute = {
|
|
4797
|
+
kind: "exact",
|
|
4798
|
+
path: NOVEL_API.plotlines,
|
|
4799
|
+
handler: async (req, res) => {
|
|
4800
|
+
if (!guard(req, res, "POST")) return;
|
|
4801
|
+
const config = getConfig();
|
|
4802
|
+
const project = requireProject(res);
|
|
4803
|
+
if (project === void 0) return;
|
|
4804
|
+
const body = await readJsonBody(req);
|
|
4805
|
+
if (project.plotlines === void 0) project.plotlines = [];
|
|
4806
|
+
const op = body?.op;
|
|
4807
|
+
if (op === "add" && body?.line !== void 0) {
|
|
4808
|
+
const line = body.line;
|
|
4809
|
+
project.plotlines.push({
|
|
4810
|
+
id: line.id !== "" ? line.id : `pl-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
4811
|
+
name: line.name.slice(0, 40),
|
|
4812
|
+
kind: line.kind,
|
|
4813
|
+
goal: line.goal.slice(0, 300),
|
|
4814
|
+
progress: line.progress.slice(0, 300),
|
|
4815
|
+
status: line.status,
|
|
4816
|
+
chapters: Array.isArray(line.chapters) ? line.chapters.filter((n) => typeof n === "number") : [],
|
|
4817
|
+
createdAt: line.createdAt !== "" ? line.createdAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
4818
|
+
});
|
|
4819
|
+
} else if (op === "update" && body?.line !== void 0 && body.line.id !== "") {
|
|
4820
|
+
const idx = project.plotlines.findIndex((l) => l.id === body.line.id);
|
|
4821
|
+
if (idx !== -1) {
|
|
4694
4822
|
const line = body.line;
|
|
4695
|
-
project.plotlines
|
|
4696
|
-
|
|
4823
|
+
project.plotlines[idx] = {
|
|
4824
|
+
...project.plotlines[idx],
|
|
4697
4825
|
name: line.name.slice(0, 40),
|
|
4698
4826
|
kind: line.kind,
|
|
4699
4827
|
goal: line.goal.slice(0, 300),
|
|
4700
4828
|
progress: line.progress.slice(0, 300),
|
|
4701
|
-
status: line.status
|
|
4702
|
-
|
|
4703
|
-
createdAt: line.createdAt !== "" ? line.createdAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
4704
|
-
});
|
|
4705
|
-
} else if (op === "update" && body?.line !== void 0 && body.line.id !== "") {
|
|
4706
|
-
const idx = project.plotlines.findIndex((l) => l.id === body.line.id);
|
|
4707
|
-
if (idx !== -1) {
|
|
4708
|
-
const line = body.line;
|
|
4709
|
-
project.plotlines[idx] = {
|
|
4710
|
-
...project.plotlines[idx],
|
|
4711
|
-
name: line.name.slice(0, 40),
|
|
4712
|
-
kind: line.kind,
|
|
4713
|
-
goal: line.goal.slice(0, 300),
|
|
4714
|
-
progress: line.progress.slice(0, 300),
|
|
4715
|
-
status: line.status
|
|
4716
|
-
};
|
|
4717
|
-
}
|
|
4718
|
-
} else if (op === "remove" && body?.id !== void 0) project.plotlines = project.plotlines.filter((l) => l.id !== body.id);
|
|
4719
|
-
else if (op === "link" && body?.id !== void 0 && typeof body.chapterNo === "number" && body.chapterNo > 0) {
|
|
4720
|
-
const line = project.plotlines.find((l) => l.id === body.id);
|
|
4721
|
-
if (line !== void 0 && !line.chapters.includes(body.chapterNo)) {
|
|
4722
|
-
line.chapters.push(body.chapterNo);
|
|
4723
|
-
if (line.status === "active" && line.progress === "") line.progress = `推进至第 ${body.chapterNo} 章`;
|
|
4724
|
-
}
|
|
4725
|
-
} else if (op === "suggest") try {
|
|
4726
|
-
const suggestions = await suggestPlotlines(ctx, config, project);
|
|
4727
|
-
writeJson(res, 200, {
|
|
4728
|
-
plotlines: project.plotlines,
|
|
4729
|
-
suggestions
|
|
4730
|
-
});
|
|
4731
|
-
return;
|
|
4732
|
-
} catch (error) {
|
|
4733
|
-
writeJson(res, 500, { error: `AI 建议失败:${error.message}` });
|
|
4734
|
-
return;
|
|
4829
|
+
status: line.status
|
|
4830
|
+
};
|
|
4735
4831
|
}
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
}
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
});
|
|
4758
|
-
return;
|
|
4759
|
-
} catch (error) {
|
|
4760
|
-
writeJson(res, 500, { error: `健康检查失败:${error.message}` });
|
|
4832
|
+
} else if (op === "remove" && body?.id !== void 0) project.plotlines = project.plotlines.filter((l) => l.id !== body.id);
|
|
4833
|
+
else if (op === "link" && body?.id !== void 0 && typeof body.chapterNo === "number" && body.chapterNo > 0) {
|
|
4834
|
+
const line = project.plotlines.find((l) => l.id === body.id);
|
|
4835
|
+
if (line !== void 0 && !line.chapters.includes(body.chapterNo)) {
|
|
4836
|
+
line.chapters.push(body.chapterNo);
|
|
4837
|
+
if (line.status === "active" && line.progress === "") line.progress = `推进至第 ${body.chapterNo} 章`;
|
|
4838
|
+
}
|
|
4839
|
+
} else if (op === "suggest") try {
|
|
4840
|
+
const suggestions = await suggestPlotlines(ctx, config, project);
|
|
4841
|
+
writeJson(res, 200, {
|
|
4842
|
+
plotlines: project.plotlines,
|
|
4843
|
+
suggestions
|
|
4844
|
+
});
|
|
4845
|
+
return;
|
|
4846
|
+
} catch (error) {
|
|
4847
|
+
writeJson(res, 500, { error: `AI 建议失败:${error.message}` });
|
|
4848
|
+
return;
|
|
4849
|
+
}
|
|
4850
|
+
else if (op === "refresh" && body?.id !== void 0) {
|
|
4851
|
+
const line = project.plotlines.find((l) => l.id === body.id);
|
|
4852
|
+
if (line === void 0) {
|
|
4853
|
+
writeJson(res, 404, { error: "剧情线不存在" });
|
|
4761
4854
|
return;
|
|
4762
4855
|
}
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
health,
|
|
4769
|
-
plan
|
|
4770
|
-
});
|
|
4856
|
+
try {
|
|
4857
|
+
line.progress = await refreshPlotlineProgress(ctx, config, project, line);
|
|
4858
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4859
|
+
saveProject(config.outputDir, project);
|
|
4860
|
+
writeJson(res, 200, { plotlines: project.plotlines });
|
|
4771
4861
|
return;
|
|
4772
4862
|
} catch (error) {
|
|
4773
|
-
writeJson(res, 500, { error:
|
|
4863
|
+
writeJson(res, 500, { error: `刷新进度失败:${error.message}` });
|
|
4774
4864
|
return;
|
|
4775
4865
|
}
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
writeJson(res, 200, {
|
|
4866
|
+
} else if (op === "health") try {
|
|
4867
|
+
const health = await analyzePlotlineHealth(ctx, config, project);
|
|
4868
|
+
writeJson(res, 200, {
|
|
4869
|
+
plotlines: project.plotlines,
|
|
4870
|
+
health
|
|
4871
|
+
});
|
|
4872
|
+
return;
|
|
4873
|
+
} catch (error) {
|
|
4874
|
+
writeJson(res, 500, { error: `健康检查失败:${error.message}` });
|
|
4875
|
+
return;
|
|
4779
4876
|
}
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4877
|
+
else if (op === "plan") try {
|
|
4878
|
+
const health = await analyzePlotlineHealth(ctx, config, project);
|
|
4879
|
+
const plan = await designPlotlinePlan(ctx, config, project, health);
|
|
4880
|
+
writeJson(res, 200, {
|
|
4881
|
+
plotlines: project.plotlines,
|
|
4882
|
+
health,
|
|
4883
|
+
plan
|
|
4884
|
+
});
|
|
4885
|
+
return;
|
|
4886
|
+
} catch (error) {
|
|
4887
|
+
writeJson(res, 500, { error: `剧情方案生成失败:${error.message}` });
|
|
4888
|
+
return;
|
|
4889
|
+
}
|
|
4890
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4891
|
+
saveProject(config.outputDir, project);
|
|
4892
|
+
writeJson(res, 200, { plotlines: project.plotlines });
|
|
4893
|
+
}
|
|
4894
|
+
};
|
|
4895
|
+
/** 敏感词检查:指定章节 / 任意文本 / 全书已写章节。 */
|
|
4896
|
+
const sensitiveRoute = {
|
|
4897
|
+
kind: "exact",
|
|
4898
|
+
path: NOVEL_API.sensitiveCheck,
|
|
4899
|
+
handler: async (req, res) => {
|
|
4900
|
+
if (!guard(req, res, "POST")) return;
|
|
4901
|
+
const config = getConfig();
|
|
4902
|
+
const project = requireProject(res);
|
|
4903
|
+
if (project === void 0) return;
|
|
4904
|
+
const body = await readJsonBody(req);
|
|
4905
|
+
const hits = [];
|
|
4906
|
+
let scanned = 0;
|
|
4907
|
+
if (body?.text !== void 0) for (const hit of checkSensitiveText(body.text)) hits.push({
|
|
4908
|
+
chapterNo: 0,
|
|
4909
|
+
word: hit.word,
|
|
4910
|
+
category: hit.category,
|
|
4911
|
+
count: hit.count
|
|
4912
|
+
});
|
|
4913
|
+
else if (typeof body?.chapterNo === "number") {
|
|
4914
|
+
const chapter = project.chapters.find((c) => c.no === body.chapterNo);
|
|
4915
|
+
if (chapter !== void 0) {
|
|
4916
|
+
const text = readChapterFile(config.outputDir, chapter);
|
|
4917
|
+
if (text !== void 0) {
|
|
4918
|
+
scanned = 1;
|
|
4919
|
+
for (const hit of checkSensitiveText(text)) hits.push({
|
|
4920
|
+
chapterNo: chapter.no,
|
|
4921
|
+
word: hit.word,
|
|
4922
|
+
category: hit.category,
|
|
4923
|
+
count: hit.count
|
|
4924
|
+
});
|
|
4925
|
+
}
|
|
4926
|
+
}
|
|
4927
|
+
} else if (body?.all === true) for (const chapter of project.chapters) {
|
|
4928
|
+
if (chapter.status === "pending" || chapter.status === "generating") continue;
|
|
4929
|
+
const text = readChapterFile(config.outputDir, chapter);
|
|
4930
|
+
if (text === void 0) continue;
|
|
4931
|
+
scanned++;
|
|
4932
|
+
for (const hit of checkSensitiveText(text)) hits.push({
|
|
4933
|
+
chapterNo: chapter.no,
|
|
4794
4934
|
word: hit.word,
|
|
4795
4935
|
category: hit.category,
|
|
4796
4936
|
count: hit.count
|
|
4797
4937
|
});
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
}
|
|
4824
|
-
else {
|
|
4825
|
-
writeJson(res, 400, { error: "请提供 chapterNo / text / all 之一" });
|
|
4826
|
-
return;
|
|
4827
|
-
}
|
|
4938
|
+
}
|
|
4939
|
+
else {
|
|
4940
|
+
writeJson(res, 400, { error: "请提供 chapterNo / text / all 之一" });
|
|
4941
|
+
return;
|
|
4942
|
+
}
|
|
4943
|
+
writeJson(res, 200, {
|
|
4944
|
+
hits,
|
|
4945
|
+
scannedChapters: scanned
|
|
4946
|
+
});
|
|
4947
|
+
}
|
|
4948
|
+
};
|
|
4949
|
+
/** 角色库:AI 提炼 / 采纳 / 更新 / 删除。 */
|
|
4950
|
+
const rolesRoute = {
|
|
4951
|
+
kind: "exact",
|
|
4952
|
+
path: NOVEL_API.roles,
|
|
4953
|
+
handler: async (req, res) => {
|
|
4954
|
+
if (!guard(req, res, "POST")) return;
|
|
4955
|
+
const config = getConfig();
|
|
4956
|
+
const project = requireProject(res);
|
|
4957
|
+
if (project === void 0) return;
|
|
4958
|
+
const body = await readJsonBody(req);
|
|
4959
|
+
if (project.roles === void 0) project.roles = [];
|
|
4960
|
+
const op = body?.op;
|
|
4961
|
+
if (op === "extract") try {
|
|
4962
|
+
const candidates = await extractRoles(ctx, config, project);
|
|
4828
4963
|
writeJson(res, 200, {
|
|
4829
|
-
|
|
4830
|
-
|
|
4964
|
+
roles: project.roles,
|
|
4965
|
+
candidates
|
|
4831
4966
|
});
|
|
4967
|
+
return;
|
|
4968
|
+
} catch (error) {
|
|
4969
|
+
writeJson(res, 500, { error: `角色提炼失败:${error.message}` });
|
|
4970
|
+
return;
|
|
4832
4971
|
}
|
|
4833
|
-
|
|
4972
|
+
else if ((op === "adopt" || op === "update") && body?.role !== void 0) {
|
|
4973
|
+
const r = body.role;
|
|
4974
|
+
const idx = project.roles.findIndex((x) => x.name === r.name);
|
|
4975
|
+
if (idx === -1) project.roles.push(r);
|
|
4976
|
+
else project.roles[idx] = r;
|
|
4977
|
+
} else if (op === "remove" && body?.name !== void 0) project.roles = project.roles.filter((x) => x.name !== body.name);
|
|
4978
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4979
|
+
saveProject(config.outputDir, project);
|
|
4980
|
+
writeJson(res, 200, { roles: project.roles });
|
|
4981
|
+
}
|
|
4982
|
+
};
|
|
4983
|
+
/** 章节复位:generating 卡死 → pending(可重新生成)。 */
|
|
4984
|
+
const chapterResetRoute = {
|
|
4985
|
+
kind: "exact",
|
|
4986
|
+
path: NOVEL_API.chapterReset,
|
|
4987
|
+
handler: async (req, res) => {
|
|
4988
|
+
if (!guard(req, res, "POST")) return;
|
|
4989
|
+
const config = getConfig();
|
|
4990
|
+
const project = requireProject(res);
|
|
4991
|
+
if (project === void 0) return;
|
|
4992
|
+
const no = (await readJsonBody(req))?.chapterNo;
|
|
4993
|
+
if (!Number.isInteger(no) || no === void 0 || no < 1) {
|
|
4994
|
+
writeJson(res, 400, { error: "chapterNo 须为正整数" });
|
|
4995
|
+
return;
|
|
4996
|
+
}
|
|
4997
|
+
const chapter = project.chapters.find((c) => c.no === no);
|
|
4998
|
+
if (chapter === void 0) {
|
|
4999
|
+
writeJson(res, 404, { error: `章节 ${no} 不在计划中` });
|
|
5000
|
+
return;
|
|
5001
|
+
}
|
|
5002
|
+
chapter.status = "pending";
|
|
5003
|
+
chapter.error = void 0;
|
|
5004
|
+
chapter.generatingAt = void 0;
|
|
5005
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5006
|
+
saveProject(config.outputDir, project);
|
|
5007
|
+
writeJson(res, 200, {
|
|
5008
|
+
ok: true,
|
|
5009
|
+
no
|
|
5010
|
+
});
|
|
5011
|
+
}
|
|
5012
|
+
};
|
|
5013
|
+
return [
|
|
5014
|
+
statusRoute,
|
|
5015
|
+
loadOutlineRoute,
|
|
5016
|
+
saveOutlineRoute,
|
|
5017
|
+
bibleRoute,
|
|
5018
|
+
volumesRoute,
|
|
5019
|
+
planRoute,
|
|
5020
|
+
generateRoute,
|
|
5021
|
+
reviewRoute,
|
|
5022
|
+
rewriteRoute,
|
|
5023
|
+
polishRoute,
|
|
5024
|
+
draftApplyRoute,
|
|
5025
|
+
draftDiscardRoute,
|
|
5026
|
+
summaryRoute,
|
|
5027
|
+
foreshadowRoute,
|
|
5028
|
+
exportRoute,
|
|
5029
|
+
chapterRoute,
|
|
5030
|
+
chapterCheckRoute,
|
|
5031
|
+
chapterSaveRoute,
|
|
5032
|
+
assetsRoute,
|
|
5033
|
+
styleEngineRoute,
|
|
5034
|
+
assistantRoute,
|
|
5035
|
+
assistantHistoryRoute,
|
|
5036
|
+
assistantClearRoute,
|
|
5037
|
+
bookshelfRoute,
|
|
5038
|
+
bookshelfActivateRoute,
|
|
5039
|
+
bookshelfRemoveRoute,
|
|
5040
|
+
resetRoute,
|
|
5041
|
+
auditRoute,
|
|
5042
|
+
charactersRefreshRoute,
|
|
5043
|
+
factsBackfillRoute,
|
|
5044
|
+
biblePatchRoute,
|
|
5045
|
+
blurbRoute,
|
|
5046
|
+
coverRoute,
|
|
5047
|
+
worldRoute,
|
|
5048
|
+
renameRoute,
|
|
5049
|
+
plotlinesRoute,
|
|
5050
|
+
rolesRoute,
|
|
5051
|
+
sensitiveRoute,
|
|
4834
5052
|
{
|
|
4835
5053
|
kind: "exact",
|
|
4836
5054
|
path: NOVEL_API.reviewBackfill,
|
|
@@ -4914,6 +5132,7 @@ function makeRoutes(deps) {
|
|
|
4914
5132
|
res.end();
|
|
4915
5133
|
}
|
|
4916
5134
|
},
|
|
5135
|
+
chapterResetRoute,
|
|
4917
5136
|
{
|
|
4918
5137
|
kind: "exact",
|
|
4919
5138
|
path: NOVEL_API.config,
|