@waterwx/dsh-novel-forge 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -202
- package/README.md +20 -4
- package/lib/client.js +637 -619
- package/lib/index.js +1424 -1127
- package/lib/types/engine.d.ts +3 -8
- package/lib/types/prompt-cache.d.ts +43 -0
- package/lib/types/prompts/adapt.d.ts +21 -0
- package/lib/types/prompts/audit.d.ts +15 -0
- package/lib/types/prompts/bible.d.ts +10 -0
- package/lib/types/prompts/catalog.d.ts +40 -0
- package/lib/types/prompts/characters.d.ts +19 -0
- package/lib/types/prompts/common.d.ts +9 -0
- package/lib/types/prompts/digest.d.ts +27 -0
- package/lib/types/prompts/facts.d.ts +26 -0
- package/lib/types/prompts/foreshadow.d.ts +10 -0
- package/lib/types/prompts/market.d.ts +18 -0
- package/lib/types/prompts/outline.d.ts +25 -0
- package/lib/types/prompts/plan.d.ts +22 -0
- package/lib/types/prompts/plotlines.d.ts +21 -0
- package/lib/types/prompts/polish.d.ts +22 -0
- package/lib/types/prompts/review.d.ts +30 -0
- package/lib/types/prompts/world.d.ts +13 -0
- package/lib/types/prompts/writing.d.ts +29 -0
- package/lib/types/protocol.d.ts +4 -0
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -3741,6 +3741,7 @@ const usageTotals = {
|
|
|
3741
3741
|
inputTokens: 0,
|
|
3742
3742
|
outputTokens: 0,
|
|
3743
3743
|
reasoningTokens: 0,
|
|
3744
|
+
cacheReadTokens: 0,
|
|
3744
3745
|
totalTokens: 0,
|
|
3745
3746
|
elapsedMs: 0,
|
|
3746
3747
|
callsWithoutUsage: 0,
|
|
@@ -3788,6 +3789,7 @@ function recordUsage(frame) {
|
|
|
3788
3789
|
usageTotals.inputTokens += usage.inputTokens;
|
|
3789
3790
|
usageTotals.outputTokens += usage.outputTokens;
|
|
3790
3791
|
usageTotals.reasoningTokens += usage.reasoningTokens ?? 0;
|
|
3792
|
+
usageTotals.cacheReadTokens += usage.cacheReadTokens ?? 0;
|
|
3791
3793
|
usageTotals.totalTokens += usage.totalTokens ?? usage.inputTokens + usage.outputTokens;
|
|
3792
3794
|
}
|
|
3793
3795
|
const label = frame.label ?? "未标注调用";
|
|
@@ -3798,6 +3800,7 @@ function recordUsage(frame) {
|
|
|
3798
3800
|
inputTokens: 0,
|
|
3799
3801
|
outputTokens: 0,
|
|
3800
3802
|
reasoningTokens: 0,
|
|
3803
|
+
cacheReadTokens: 0,
|
|
3801
3804
|
elapsedMs: 0
|
|
3802
3805
|
};
|
|
3803
3806
|
bucket.calls += 1;
|
|
@@ -3807,6 +3810,7 @@ function recordUsage(frame) {
|
|
|
3807
3810
|
bucket.inputTokens += usage.inputTokens;
|
|
3808
3811
|
bucket.outputTokens += usage.outputTokens;
|
|
3809
3812
|
bucket.reasoningTokens += usage.reasoningTokens ?? 0;
|
|
3813
|
+
bucket.cacheReadTokens = (bucket.cacheReadTokens ?? 0) + (usage.cacheReadTokens ?? 0);
|
|
3810
3814
|
}
|
|
3811
3815
|
usageTotals.byLabel.set(label, bucket);
|
|
3812
3816
|
}
|
|
@@ -3819,6 +3823,7 @@ function liveUsage() {
|
|
|
3819
3823
|
outputTokens: usageTotals.outputTokens,
|
|
3820
3824
|
reasoningTokens: usageTotals.reasoningTokens,
|
|
3821
3825
|
totalTokens: usageTotals.totalTokens,
|
|
3826
|
+
cacheReadTokens: usageTotals.cacheReadTokens,
|
|
3822
3827
|
elapsedMs: usageTotals.elapsedMs,
|
|
3823
3828
|
callsWithoutUsage: usageTotals.callsWithoutUsage,
|
|
3824
3829
|
byLabel: [...usageTotals.byLabel.values()].sort((a, b) => b.inputTokens + b.outputTokens + b.reasoningTokens - (a.inputTokens + a.outputTokens + a.reasoningTokens))
|
|
@@ -3831,6 +3836,7 @@ function resetLiveUsage() {
|
|
|
3831
3836
|
usageTotals.inputTokens = 0;
|
|
3832
3837
|
usageTotals.outputTokens = 0;
|
|
3833
3838
|
usageTotals.reasoningTokens = 0;
|
|
3839
|
+
usageTotals.cacheReadTokens = 0;
|
|
3834
3840
|
usageTotals.totalTokens = 0;
|
|
3835
3841
|
usageTotals.elapsedMs = 0;
|
|
3836
3842
|
usageTotals.callsWithoutUsage = 0;
|
|
@@ -4445,428 +4451,76 @@ function renderLengthContract(entries) {
|
|
|
4445
4451
|
if (entries.length === 0) return "";
|
|
4446
4452
|
return `为保证输出完整:${entries.map(([label, limit]) => `${label}不超过 ${limit} 字`).join(";")}。每个字段只保留完成本阶段决策所需的信息,写完后立即结束输出,不要展开细纲或重复说明。`;
|
|
4447
4453
|
}
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
],
|
|
4467
|
-
suspense: [
|
|
4468
|
-
30,
|
|
4469
|
-
42,
|
|
4470
|
-
38,
|
|
4471
|
-
50,
|
|
4472
|
-
46,
|
|
4473
|
-
58,
|
|
4474
|
-
62,
|
|
4475
|
-
56,
|
|
4476
|
-
70,
|
|
4477
|
-
78,
|
|
4478
|
-
86,
|
|
4479
|
-
74
|
|
4480
|
-
],
|
|
4481
|
-
wave: [
|
|
4482
|
-
45,
|
|
4483
|
-
30,
|
|
4484
|
-
55,
|
|
4485
|
-
34,
|
|
4486
|
-
62,
|
|
4487
|
-
40,
|
|
4488
|
-
70,
|
|
4489
|
-
46,
|
|
4490
|
-
78,
|
|
4491
|
-
52,
|
|
4492
|
-
84,
|
|
4493
|
-
60
|
|
4494
|
-
],
|
|
4495
|
-
frontLoad: [
|
|
4496
|
-
78,
|
|
4497
|
-
66,
|
|
4498
|
-
58,
|
|
4499
|
-
50,
|
|
4500
|
-
46,
|
|
4501
|
-
44,
|
|
4502
|
-
42,
|
|
4503
|
-
44,
|
|
4504
|
-
46,
|
|
4505
|
-
48,
|
|
4506
|
-
50,
|
|
4507
|
-
52
|
|
4508
|
-
],
|
|
4509
|
-
flat: [
|
|
4510
|
-
40,
|
|
4511
|
-
42,
|
|
4512
|
-
44,
|
|
4513
|
-
42,
|
|
4514
|
-
46,
|
|
4515
|
-
44,
|
|
4516
|
-
48,
|
|
4517
|
-
46,
|
|
4518
|
-
50,
|
|
4519
|
-
48,
|
|
4520
|
-
52,
|
|
4521
|
-
50
|
|
4522
|
-
]
|
|
4523
|
-
};
|
|
4524
|
-
/** 参考曲线的中文标签(面板展示)。 */
|
|
4525
|
-
const TENSION_PRESET_LABELS = {
|
|
4526
|
-
escalation: "递进爬升",
|
|
4527
|
-
suspense: "悬疑加压",
|
|
4528
|
-
wave: "波浪呼吸",
|
|
4529
|
-
frontLoad: "前紧后稳",
|
|
4530
|
-
flat: "平稳推进",
|
|
4531
|
-
custom: "自定义"
|
|
4532
|
-
};
|
|
4533
|
-
/** 全部可选预设(UI 下拉用)。 */
|
|
4534
|
-
const TENSION_PRESETS = [
|
|
4535
|
-
"escalation",
|
|
4536
|
-
"suspense",
|
|
4537
|
-
"wave",
|
|
4538
|
-
"frontLoad",
|
|
4539
|
-
"flat",
|
|
4540
|
-
"custom"
|
|
4541
|
-
];
|
|
4542
|
-
/** 把参考形状按章节数重采样(最近邻,保持形状不引入平滑失真)。 */
|
|
4543
|
-
function referenceCurve(preset, chapterCount) {
|
|
4544
|
-
if (chapterCount <= 0) return [];
|
|
4545
|
-
if (preset === "custom") return [];
|
|
4546
|
-
const shape = REFERENCE_SHAPES[preset];
|
|
4547
|
-
if (chapterCount === 1) return [shape[Math.floor(shape.length / 2)] ?? 50];
|
|
4548
|
-
return Array.from({ length: chapterCount }, (_, i) => {
|
|
4549
|
-
const pos = i / (chapterCount - 1) * (shape.length - 1);
|
|
4550
|
-
const lo = Math.floor(pos);
|
|
4551
|
-
const hi = Math.min(shape.length - 1, lo + 1);
|
|
4552
|
-
const t = pos - lo;
|
|
4553
|
-
const a = shape[lo] ?? 50;
|
|
4554
|
-
const b = shape[hi] ?? a;
|
|
4555
|
-
return Math.round(a + (b - a) * t);
|
|
4556
|
-
});
|
|
4557
|
-
}
|
|
4558
|
-
/** 读一章的目标张力(未设置时返回 undefined)。 */
|
|
4559
|
-
function targetTension(chapter) {
|
|
4560
|
-
return typeof chapter.tension === "number" ? clampTension(chapter.tension) : void 0;
|
|
4561
|
-
}
|
|
4562
|
-
/** 读一章的实际张力(审稿时模型打分,未设置时返回 undefined)。 */
|
|
4563
|
-
function actualTension(chapter) {
|
|
4564
|
-
const fromReview = chapter.review?.tension;
|
|
4565
|
-
if (typeof fromReview === "number") return clampTension(fromReview);
|
|
4566
|
-
return typeof chapter.tensionActual === "number" ? clampTension(chapter.tensionActual) : void 0;
|
|
4454
|
+
//#endregion
|
|
4455
|
+
//#region src/timeline.ts
|
|
4456
|
+
/** 时间线矛盾初筛的最少事件数(太少没有意义)。 */
|
|
4457
|
+
const MIN_EVENTS_FOR_CHECK = 2;
|
|
4458
|
+
/** 归一化事件(清洗字符串、补 id/时间戳)。 */
|
|
4459
|
+
function normalizeTimelineEvent(raw, chapterNo, index, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
4460
|
+
const characters = Array.isArray(raw.characters) ? raw.characters.filter((c) => typeof c === "string" && c.trim() !== "") : [];
|
|
4461
|
+
return {
|
|
4462
|
+
id: typeof raw.id === "string" && raw.id !== "" ? raw.id : `tl-${chapterNo}-${index}-${Math.random().toString(36).slice(2, 8)}`,
|
|
4463
|
+
chapterNo: typeof raw.chapterNo === "number" ? raw.chapterNo : chapterNo,
|
|
4464
|
+
time: (raw.time ?? "").trim(),
|
|
4465
|
+
order: typeof raw.order === "number" && Number.isFinite(raw.order) ? raw.order : void 0,
|
|
4466
|
+
place: (raw.place ?? "").trim(),
|
|
4467
|
+
characters: characters.map((c) => c.trim()),
|
|
4468
|
+
event: (raw.event ?? "").trim(),
|
|
4469
|
+
source: raw.source === "manual" ? "manual" : "extracted",
|
|
4470
|
+
createdAt: raw.createdAt ?? now
|
|
4471
|
+
};
|
|
4567
4472
|
}
|
|
4568
|
-
/**
|
|
4569
|
-
function
|
|
4570
|
-
|
|
4571
|
-
|
|
4473
|
+
/** 时间线排序:先章节、再 order(缺省排在该章末尾)、最后 createdAt。 */
|
|
4474
|
+
function sortTimeline(events) {
|
|
4475
|
+
return [...events].sort((a, b) => {
|
|
4476
|
+
if (a.chapterNo !== b.chapterNo) return a.chapterNo - b.chapterNo;
|
|
4477
|
+
const ao = a.order ?? Number.MAX_SAFE_INTEGER;
|
|
4478
|
+
const bo = b.order ?? Number.MAX_SAFE_INTEGER;
|
|
4479
|
+
if (ao !== bo) return ao - bo;
|
|
4480
|
+
return a.createdAt.localeCompare(b.createdAt);
|
|
4481
|
+
});
|
|
4572
4482
|
}
|
|
4573
4483
|
/**
|
|
4574
|
-
*
|
|
4484
|
+
* 渲染注入到「写作/规划」提示词的时间线锚点块。
|
|
4575
4485
|
*
|
|
4576
4486
|
* @param project 项目状态。
|
|
4577
|
-
* @param
|
|
4578
|
-
* @returns
|
|
4487
|
+
* @param beforeChapter 只注入该章之前的锚点(含该章本身不注入,避免剧透)。
|
|
4488
|
+
* @returns 提示词片段(无事件时返回空串)。带显式截断声明。
|
|
4579
4489
|
*/
|
|
4580
|
-
function
|
|
4581
|
-
const
|
|
4582
|
-
|
|
4583
|
-
const
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
parts.push(delta >= 10 ? "比上一章明显更紧" : delta <= -10 ? "比上一章明显放松(给读者呼吸口)" : "与上一章持平");
|
|
4594
|
-
}
|
|
4595
|
-
if (ref !== void 0) parts.push(`参考曲线(${TENSION_PRESET_LABELS[preset ?? "custom"]})在此处约为 ${ref}`);
|
|
4596
|
-
if (parts.length === 0) return "";
|
|
4597
|
-
return [
|
|
4598
|
-
"==================== 本章张力(写作时把握松紧) ====================",
|
|
4599
|
-
parts.join(";") + "。",
|
|
4600
|
-
"低张力段用于铺垫关系、交代信息、留呼吸;高张力段用于对抗、揭示、抉择与代价。",
|
|
4601
|
-
"同一章内可以有起伏,但整章的平均松紧要贴近目标值;不要连续多章都停在同一个紧张度。"
|
|
4602
|
-
].join("\n");
|
|
4490
|
+
function renderTimelineBlock(project, beforeChapter) {
|
|
4491
|
+
const all = sortTimeline(project.timeline ?? []).filter((e) => e.chapterNo < beforeChapter);
|
|
4492
|
+
if (all.length === 0) return "";
|
|
4493
|
+
const shown = all.slice(-12);
|
|
4494
|
+
const omitted = all.length - shown.length;
|
|
4495
|
+
return `${omitted > 0 ? `【故事时间线(共 ${all.length} 条,此处只列最近 ${shown.length} 条;更早 ${omitted} 条未列出)】` : `【故事时间线(共 ${shown.length} 条)】`}\n${shown.map((e) => {
|
|
4496
|
+
const parts = [`[第${e.chapterNo}章]`];
|
|
4497
|
+
if (e.time !== "") parts.push(e.time);
|
|
4498
|
+
if (e.place !== "") parts.push(`@${e.place}`);
|
|
4499
|
+
if (e.characters.length > 0) parts.push(`(${e.characters.join("、")})`);
|
|
4500
|
+
parts.push(e.event);
|
|
4501
|
+
return "- " + parts.join(" ");
|
|
4502
|
+
}).join("\n")}\n写作时必须与前文时间顺序、地点衔接一致;不得让时间倒流或让人物瞬移。`;
|
|
4603
4503
|
}
|
|
4604
4504
|
/**
|
|
4605
|
-
*
|
|
4505
|
+
* 规则初筛时间线矛盾(不调模型,瞬时计算)。
|
|
4606
4506
|
*
|
|
4607
|
-
*
|
|
4608
|
-
* 1)
|
|
4609
|
-
* 2)
|
|
4610
|
-
* 3)
|
|
4611
|
-
* 4) 长期低位(连续 ≥5 章 <40,容易拖沓)。
|
|
4507
|
+
* 覆盖三类最容易犯又最容易用规则抓到的错:
|
|
4508
|
+
* 1) 同一章内事件顺序号倒退;
|
|
4509
|
+
* 2) 全书顺序号在章节推进后倒退(时间倒流);
|
|
4510
|
+
* 3) 相邻两章之间地点瞬移(同一角色在未交代移动的情况下跨地点)。
|
|
4612
4511
|
*
|
|
4613
4512
|
* @param project 项目状态。
|
|
4614
4513
|
* @returns 问题清单(可能为空)。
|
|
4615
4514
|
*/
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
* 面板要能在每一章上给出「一键按建议修订」,合并层也要能把建议落到中间章。
|
|
4620
|
-
*/
|
|
4621
|
-
function runChapters(from, to) {
|
|
4622
|
-
const out = [];
|
|
4623
|
-
for (let no = from; no <= to; no++) out.push(no);
|
|
4624
|
-
return out;
|
|
4625
|
-
}
|
|
4626
|
-
function detectTensionIssues(project) {
|
|
4627
|
-
const values = [...project.chapters ?? []].sort((a, b) => a.no - b.no).map((c) => ({
|
|
4628
|
-
no: c.no,
|
|
4629
|
-
target: targetTension(c),
|
|
4630
|
-
actual: actualTension(c)
|
|
4631
|
-
}));
|
|
4632
|
-
if (values.filter((v) => v.target !== void 0 || v.actual !== void 0).length < 3) return [];
|
|
4515
|
+
function detectTimelineIssues(project) {
|
|
4516
|
+
const stored = project.timeline ?? [];
|
|
4517
|
+
if (stored.length < MIN_EVENTS_FOR_CHECK) return [];
|
|
4633
4518
|
const issues = [];
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
const
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
chapters: [v.no],
|
|
4640
|
-
item: `第${v.no}章张力偏差较大:目标 ${v.target},实际 ${v.actual}(${gap > 0 ? "比目标更紧" : "比目标更松"})`,
|
|
4641
|
-
suggestion: gap > 0 ? "本章对抗/揭示密度偏高,考虑把部分冲突后移,留出呼吸段。" : "本章推进偏平,考虑加一处对抗、揭示或代价。"
|
|
4642
|
-
});
|
|
4643
|
-
}
|
|
4644
|
-
let run = 1;
|
|
4645
|
-
for (let i = 1; i < values.length; i++) {
|
|
4646
|
-
const a = values[i - 1].target ?? values[i - 1].actual;
|
|
4647
|
-
const b = values[i].target ?? values[i].actual;
|
|
4648
|
-
if (a !== void 0 && b !== void 0 && Math.abs(b - a) < 6) {
|
|
4649
|
-
run++;
|
|
4650
|
-
if (run === 4) {
|
|
4651
|
-
const from = values[i - run + 1].no;
|
|
4652
|
-
issues.push({
|
|
4653
|
-
severity: "medium",
|
|
4654
|
-
chapters: runChapters(from, values[i].no),
|
|
4655
|
-
item: `第${from}-${values[i].no}章张力几乎无变化(连续 ${run} 章)`,
|
|
4656
|
-
suggestion: "安排一次明显抬升或一次释放,让曲线有起伏,避免读者疲劳。"
|
|
4657
|
-
});
|
|
4658
|
-
}
|
|
4659
|
-
} else run = 1;
|
|
4660
|
-
}
|
|
4661
|
-
let highRun = 0;
|
|
4662
|
-
for (const v of values) {
|
|
4663
|
-
const val = v.target ?? v.actual;
|
|
4664
|
-
if (val !== void 0 && val >= 80) {
|
|
4665
|
-
highRun++;
|
|
4666
|
-
if (highRun === 3) issues.push({
|
|
4667
|
-
severity: "medium",
|
|
4668
|
-
chapters: runChapters(v.no - 2, v.no),
|
|
4669
|
-
item: `连续 ${highRun} 章处在高位(≥80),缺少呼吸口`,
|
|
4670
|
-
suggestion: "高潮之后补一章缓冲(关系推进、信息消化或日常),再进入下一轮对抗。"
|
|
4671
|
-
});
|
|
4672
|
-
} else highRun = 0;
|
|
4673
|
-
}
|
|
4674
|
-
let lowRun = 0;
|
|
4675
|
-
for (const v of values) {
|
|
4676
|
-
const val = v.target ?? v.actual;
|
|
4677
|
-
if (val !== void 0 && val < 40) {
|
|
4678
|
-
lowRun++;
|
|
4679
|
-
if (lowRun === 5) issues.push({
|
|
4680
|
-
severity: "high",
|
|
4681
|
-
chapters: runChapters(v.no - 4, v.no),
|
|
4682
|
-
item: `连续 ${lowRun} 章张力低于 40,推进可能偏慢`,
|
|
4683
|
-
suggestion: "提前兑现一个目标、抛出一个新风险,或让配角线产生冲撞。"
|
|
4684
|
-
});
|
|
4685
|
-
} else lowRun = 0;
|
|
4686
|
-
}
|
|
4687
|
-
return issues;
|
|
4688
|
-
}
|
|
4689
|
-
/**
|
|
4690
|
-
* 组装面板需要的曲线数据(章节号 / 目标 / 实际 / 参考)。
|
|
4691
|
-
*
|
|
4692
|
-
* @param project 项目状态。
|
|
4693
|
-
* @returns 逐章数据点与预设名。
|
|
4694
|
-
*/
|
|
4695
|
-
function buildCurveData(project) {
|
|
4696
|
-
const preset = project.tensionCurve?.preset ?? "escalation";
|
|
4697
|
-
const chapters = [...project.chapters ?? []].sort((a, b) => a.no - b.no);
|
|
4698
|
-
const custom = project.tensionCurve?.custom ?? [];
|
|
4699
|
-
const reference = preset === "custom" ? custom : referenceCurve(preset, chapters.length);
|
|
4700
|
-
return {
|
|
4701
|
-
preset,
|
|
4702
|
-
points: chapters.map((c, i) => {
|
|
4703
|
-
const point = {
|
|
4704
|
-
no: c.no,
|
|
4705
|
-
title: c.title
|
|
4706
|
-
};
|
|
4707
|
-
const t = targetTension(c);
|
|
4708
|
-
const a = actualTension(c);
|
|
4709
|
-
if (t !== void 0) point.target = t;
|
|
4710
|
-
if (a !== void 0) point.actual = a;
|
|
4711
|
-
if (reference[i] !== void 0) point.reference = reference[i];
|
|
4712
|
-
return point;
|
|
4713
|
-
})
|
|
4714
|
-
};
|
|
4715
|
-
}
|
|
4716
|
-
//#endregion
|
|
4717
|
-
//#region src/prompt-slots.ts
|
|
4718
|
-
/** 允许作者编辑的槽位(白名单;不在此列表的提示词一律不可改)。 */
|
|
4719
|
-
const PROMPT_SLOTS = [
|
|
4720
|
-
{
|
|
4721
|
-
id: "styleExtra",
|
|
4722
|
-
label: "文风补充",
|
|
4723
|
-
hint: "追加到写作提示词末尾的风格偏好(句式、叙述习惯、用词倾向)。",
|
|
4724
|
-
placeholder: "例如:多用短句和对话推进,少用成语;环境描写只写能影响动作的部分。",
|
|
4725
|
-
maxChars: 1200
|
|
4726
|
-
},
|
|
4727
|
-
{
|
|
4728
|
-
id: "antiAiExtra",
|
|
4729
|
-
label: "反 AI 补充",
|
|
4730
|
-
hint: "本书额外的\"别这么写\"清单,与内置反 AI 规则叠加(不能覆盖内置规则)。",
|
|
4731
|
-
placeholder: "例如:不要每段都用比喻;不要用\"空气中弥漫着\"这类句式。",
|
|
4732
|
-
maxChars: 1200
|
|
4733
|
-
},
|
|
4734
|
-
{
|
|
4735
|
-
id: "dialogueExtra",
|
|
4736
|
-
label: "对话补充",
|
|
4737
|
-
hint: "角色说话方式的额外约束,写入时会与角色卡一起注入。",
|
|
4738
|
-
placeholder: "例如:长辈说话带方言词;主角对熟人用绰号称呼。",
|
|
4739
|
-
maxChars: 800
|
|
4740
|
-
},
|
|
4741
|
-
{
|
|
4742
|
-
id: "endingHookExtra",
|
|
4743
|
-
label: "章末钩子偏好",
|
|
4744
|
-
hint: "对结尾钩子的额外要求(与章节计划里的 endingHook 叠加)。",
|
|
4745
|
-
placeholder: "例如:结尾尽量落在一个未闭合的选择上,而不是新出现的敌人。",
|
|
4746
|
-
maxChars: 600
|
|
4747
|
-
}
|
|
4748
|
-
];
|
|
4749
|
-
/** 槽位 id → 规格。 */
|
|
4750
|
-
const SLOT_BY_ID = new Map(PROMPT_SLOTS.map((s) => [s.id, s]));
|
|
4751
|
-
/**
|
|
4752
|
-
* 读取并规整项目里的槽位内容(去空白、按上限截断)。
|
|
4753
|
-
*
|
|
4754
|
-
* @param project 项目状态。
|
|
4755
|
-
* @returns 非空槽位列表(保持 PROMPT_SLOTS 的声明顺序)。
|
|
4756
|
-
*/
|
|
4757
|
-
function readPromptSlots(project) {
|
|
4758
|
-
const stored = project.promptSlots ?? {};
|
|
4759
|
-
const out = [];
|
|
4760
|
-
for (const spec of PROMPT_SLOTS) {
|
|
4761
|
-
const raw = (stored[spec.id] ?? "").trim();
|
|
4762
|
-
if (raw === "") continue;
|
|
4763
|
-
const truncated = raw.length > spec.maxChars;
|
|
4764
|
-
out.push({
|
|
4765
|
-
id: spec.id,
|
|
4766
|
-
label: spec.label,
|
|
4767
|
-
text: truncated ? raw.slice(0, spec.maxChars) : raw,
|
|
4768
|
-
truncated,
|
|
4769
|
-
chars: raw.length
|
|
4770
|
-
});
|
|
4771
|
-
}
|
|
4772
|
-
return out;
|
|
4773
|
-
}
|
|
4774
|
-
/**
|
|
4775
|
-
* 把槽位渲染成注入提示词的段落。
|
|
4776
|
-
*
|
|
4777
|
-
* @param project 项目状态。
|
|
4778
|
-
* @returns 提示词片段;无槽位时返回空串。
|
|
4779
|
-
*/
|
|
4780
|
-
function renderPromptSlots(project) {
|
|
4781
|
-
const slots = readPromptSlots(project);
|
|
4782
|
-
if (slots.length === 0) return "";
|
|
4783
|
-
const lines = ["==================== 作者偏好(本书自定义,优先级低于道藏/红线/合规) ===================="];
|
|
4784
|
-
for (const slot of slots) lines.push(`【${slot.label}】${slot.text}${slot.truncated ? `(已按 ${SLOT_BY_ID.get(slot.id)?.maxChars ?? 0} 字上限截断,原 ${slot.chars} 字)` : ""}`);
|
|
4785
|
-
lines.push("以上偏好只影响表达方式;与道藏、写作红线、内容合规红线冲突时,一律以后者为准。");
|
|
4786
|
-
return lines.join("\n");
|
|
4787
|
-
}
|
|
4788
|
-
/**
|
|
4789
|
-
* 校验并规整一次槽位写入。
|
|
4790
|
-
*
|
|
4791
|
-
* @param id 槽位 id。
|
|
4792
|
-
* @param value 内容(空串表示清空该槽位)。
|
|
4793
|
-
* @returns 归一化后的内容。
|
|
4794
|
-
* @throws 槽位 id 不在白名单时抛错。
|
|
4795
|
-
*/
|
|
4796
|
-
function normalizeSlotValue(id, value) {
|
|
4797
|
-
if (SLOT_BY_ID.get(id) === void 0) throw new Error(`未知提示词槽位:${id}`);
|
|
4798
|
-
return value.replace(/\r\n/g, "\n").trim();
|
|
4799
|
-
}
|
|
4800
|
-
//#endregion
|
|
4801
|
-
//#region src/timeline.ts
|
|
4802
|
-
/** 时间线矛盾初筛的最少事件数(太少没有意义)。 */
|
|
4803
|
-
const MIN_EVENTS_FOR_CHECK = 2;
|
|
4804
|
-
/** 归一化事件(清洗字符串、补 id/时间戳)。 */
|
|
4805
|
-
function normalizeTimelineEvent(raw, chapterNo, index, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
4806
|
-
const characters = Array.isArray(raw.characters) ? raw.characters.filter((c) => typeof c === "string" && c.trim() !== "") : [];
|
|
4807
|
-
return {
|
|
4808
|
-
id: typeof raw.id === "string" && raw.id !== "" ? raw.id : `tl-${chapterNo}-${index}-${Math.random().toString(36).slice(2, 8)}`,
|
|
4809
|
-
chapterNo: typeof raw.chapterNo === "number" ? raw.chapterNo : chapterNo,
|
|
4810
|
-
time: (raw.time ?? "").trim(),
|
|
4811
|
-
order: typeof raw.order === "number" && Number.isFinite(raw.order) ? raw.order : void 0,
|
|
4812
|
-
place: (raw.place ?? "").trim(),
|
|
4813
|
-
characters: characters.map((c) => c.trim()),
|
|
4814
|
-
event: (raw.event ?? "").trim(),
|
|
4815
|
-
source: raw.source === "manual" ? "manual" : "extracted",
|
|
4816
|
-
createdAt: raw.createdAt ?? now
|
|
4817
|
-
};
|
|
4818
|
-
}
|
|
4819
|
-
/** 时间线排序:先章节、再 order(缺省排在该章末尾)、最后 createdAt。 */
|
|
4820
|
-
function sortTimeline(events) {
|
|
4821
|
-
return [...events].sort((a, b) => {
|
|
4822
|
-
if (a.chapterNo !== b.chapterNo) return a.chapterNo - b.chapterNo;
|
|
4823
|
-
const ao = a.order ?? Number.MAX_SAFE_INTEGER;
|
|
4824
|
-
const bo = b.order ?? Number.MAX_SAFE_INTEGER;
|
|
4825
|
-
if (ao !== bo) return ao - bo;
|
|
4826
|
-
return a.createdAt.localeCompare(b.createdAt);
|
|
4827
|
-
});
|
|
4828
|
-
}
|
|
4829
|
-
/**
|
|
4830
|
-
* 渲染注入到「写作/规划」提示词的时间线锚点块。
|
|
4831
|
-
*
|
|
4832
|
-
* @param project 项目状态。
|
|
4833
|
-
* @param beforeChapter 只注入该章之前的锚点(含该章本身不注入,避免剧透)。
|
|
4834
|
-
* @returns 提示词片段(无事件时返回空串)。带显式截断声明。
|
|
4835
|
-
*/
|
|
4836
|
-
function renderTimelineBlock(project, beforeChapter) {
|
|
4837
|
-
const all = sortTimeline(project.timeline ?? []).filter((e) => e.chapterNo < beforeChapter);
|
|
4838
|
-
if (all.length === 0) return "";
|
|
4839
|
-
const shown = all.slice(-12);
|
|
4840
|
-
const omitted = all.length - shown.length;
|
|
4841
|
-
return `${omitted > 0 ? `【故事时间线(共 ${all.length} 条,此处只列最近 ${shown.length} 条;更早 ${omitted} 条未列出)】` : `【故事时间线(共 ${shown.length} 条)】`}\n${shown.map((e) => {
|
|
4842
|
-
const parts = [`[第${e.chapterNo}章]`];
|
|
4843
|
-
if (e.time !== "") parts.push(e.time);
|
|
4844
|
-
if (e.place !== "") parts.push(`@${e.place}`);
|
|
4845
|
-
if (e.characters.length > 0) parts.push(`(${e.characters.join("、")})`);
|
|
4846
|
-
parts.push(e.event);
|
|
4847
|
-
return "- " + parts.join(" ");
|
|
4848
|
-
}).join("\n")}\n写作时必须与前文时间顺序、地点衔接一致;不得让时间倒流或让人物瞬移。`;
|
|
4849
|
-
}
|
|
4850
|
-
/**
|
|
4851
|
-
* 规则初筛时间线矛盾(不调模型,瞬时计算)。
|
|
4852
|
-
*
|
|
4853
|
-
* 覆盖三类最容易犯又最容易用规则抓到的错:
|
|
4854
|
-
* 1) 同一章内事件顺序号倒退;
|
|
4855
|
-
* 2) 全书顺序号在章节推进后倒退(时间倒流);
|
|
4856
|
-
* 3) 相邻两章之间地点瞬移(同一角色在未交代移动的情况下跨地点)。
|
|
4857
|
-
*
|
|
4858
|
-
* @param project 项目状态。
|
|
4859
|
-
* @returns 问题清单(可能为空)。
|
|
4860
|
-
*/
|
|
4861
|
-
function detectTimelineIssues(project) {
|
|
4862
|
-
const stored = project.timeline ?? [];
|
|
4863
|
-
if (stored.length < MIN_EVENTS_FOR_CHECK) return [];
|
|
4864
|
-
const issues = [];
|
|
4865
|
-
const byChapter = /* @__PURE__ */ new Map();
|
|
4866
|
-
for (const e of stored) {
|
|
4867
|
-
const list = byChapter.get(e.chapterNo) ?? [];
|
|
4868
|
-
list.push(e);
|
|
4869
|
-
byChapter.set(e.chapterNo, list);
|
|
4519
|
+
const byChapter = /* @__PURE__ */ new Map();
|
|
4520
|
+
for (const e of stored) {
|
|
4521
|
+
const list = byChapter.get(e.chapterNo) ?? [];
|
|
4522
|
+
list.push(e);
|
|
4523
|
+
byChapter.set(e.chapterNo, list);
|
|
4870
4524
|
}
|
|
4871
4525
|
for (const [no, list] of byChapter) {
|
|
4872
4526
|
let lastOrder;
|
|
@@ -5076,26 +4730,6 @@ function pruneSnapshots(outputDir, keep = 20) {
|
|
|
5076
4730
|
return removed;
|
|
5077
4731
|
}
|
|
5078
4732
|
//#endregion
|
|
5079
|
-
//#region src/prompting.ts
|
|
5080
|
-
/** 章节写作约束:把写作骨架渲染成可附加到系统提示词末尾的必达/禁止/自查块。 */
|
|
5081
|
-
function renderChapterWriterSkeleton(meta) {
|
|
5082
|
-
const pov = meta.pov ?? "第三人称有限视角,严格跟随主角所见所知。";
|
|
5083
|
-
const tone = meta.tonePreference ?? "文风贴合本书设定,用具体细节与动作推进。";
|
|
5084
|
-
const hook = meta.endingHookPreference ?? "章末留一个明确的钩子(新信息、新风险或未闭合的选择)。";
|
|
5085
|
-
const antiAi = meta.antiAiRules ?? "";
|
|
5086
|
-
return [
|
|
5087
|
-
"==================== 章节生成骨架(必达 / 禁止 / 输出前自查) ====================",
|
|
5088
|
-
"【叙事视角】" + pov,
|
|
5089
|
-
"【本章必达】本章必须发生实质变化——局面、关系、信息、风险、决策至少动一项;开篇直接进入情境,不复述上一章。",
|
|
5090
|
-
"【禁止事项】不写以总结、复盘、解释为主的段落;不新增核心角色,不写入与上下文冲突的设定;不为凑字数注水。",
|
|
5091
|
-
"【篇幅】目标 " + meta.targetChars + " 字,区间 " + meta.minChars + "-" + meta.maxChars + " 字;不够就继续推进有效情节,不许草草收尾。",
|
|
5092
|
-
"【结尾】" + hook,
|
|
5093
|
-
"【表达】用具体动作、对话与可感知细节往前推;" + tone,
|
|
5094
|
-
"【反 AI】" + antiAi,
|
|
5095
|
-
"【输出前自查】逐项确认:读者的回报、关键转折、章末净变化是否看得见?上一章留下的钩子有没有交代?人物的硬事实有没有被写反?确认无误再输出,正文里不要出现核查过程。"
|
|
5096
|
-
].join("\n");
|
|
5097
|
-
}
|
|
5098
|
-
//#endregion
|
|
5099
4733
|
//#region src/action-guard.ts
|
|
5100
4734
|
/** 带分级的动作失败(executeAction 抛出,供 fail-stop 判定)。 */
|
|
5101
4735
|
var NovelActionError = class extends Error {
|
|
@@ -5351,6 +4985,1302 @@ function renderContextBlocks(ctx) {
|
|
|
5351
4985
|
};
|
|
5352
4986
|
}
|
|
5353
4987
|
//#endregion
|
|
4988
|
+
//#region src/prompts/bible.ts
|
|
4989
|
+
/** System prompt for story-bible extraction. */
|
|
4990
|
+
function bibleSystemPrompt() {
|
|
4991
|
+
return [
|
|
4992
|
+
"你是一位资深网文编辑兼设定架构师。你会收到一份小说大纲,请把它提炼成结构化的「道藏」,供后续写作时严格引用。",
|
|
4993
|
+
"要求:",
|
|
4994
|
+
"1. 忠于大纲,不自行发明大纲之外的设定。",
|
|
4995
|
+
"2. 角色卡覆盖大纲明确出现的角色(主角必含),每个角色给出性格标签、目标、关键关系。",
|
|
4996
|
+
"3. 世界规则覆盖力量体系、金手指机制、势力、地理等所有硬性规则,逐条列出。",
|
|
4997
|
+
"4. 红线列出大纲中明确禁止的内容(如无后宫、不圣母、无无脑碾压等)。",
|
|
4998
|
+
"5. 风格列出叙事基调、节奏、POV 等写作风格要点。",
|
|
4999
|
+
"6. 角色名必须用正文/编年录中的真实姓名;若大纲只写「主角」未点名,而已写章节或编年录中有名字,则用该真实姓名;禁止输出「主角(描述)」这类把身份塞进名字的占位名。",
|
|
5000
|
+
"输出必须是合法 JSON 对象,不要输出任何其他文字或 Markdown 代码块标记。",
|
|
5001
|
+
"重要:所有字符串值内部不得包含换行符(不要用多行字符串),JSON 必须在一段内完整结束。",
|
|
5002
|
+
"重要:直接输出 JSON 结果本身,不要把思考过程或推理内容写在输出里。",
|
|
5003
|
+
"JSON 结构:",
|
|
5004
|
+
"{\"genre\": \"题材与基调一句话\", \"worldRules\": [\"规则1\", \"规则2\", ...], \"characters\": [{\"name\": \"角色名\", \"role\": \"protagonist|supporting|antagonist|other\", \"traits\": [\"标签1\", ...], \"goals\": \"目标与动机\", \"relations\": \"关键关系\"}], \"redLines\": [\"红线1\", ...], \"style\": [\"风格1\", ...]}"
|
|
5005
|
+
].join("\n");
|
|
5006
|
+
}
|
|
5007
|
+
//#endregion
|
|
5008
|
+
//#region src/prompts/plan.ts
|
|
5009
|
+
/** System prompt for volume planning. */
|
|
5010
|
+
function volumeSystemPrompt() {
|
|
5011
|
+
return [
|
|
5012
|
+
"你是一位资深网文总编。你会收到一份小说大纲,请把全书划分为若干「卷」(分卷),每卷有明确的剧情定位与起止章节。",
|
|
5013
|
+
"要求:",
|
|
5014
|
+
"1. 大纲已有分卷时,严格遵循大纲的分卷结构;没有时按剧情弧线合理划分(3-8 卷)。",
|
|
5015
|
+
"2. 卷定位一句话说明该卷的剧情重心。",
|
|
5016
|
+
"3. chapterStart/chapterEnd 给出该卷覆盖的章节区间(从 1 开始连续编号)。",
|
|
5017
|
+
"输出必须是合法 JSON 数组,不要输出任何其他文字:",
|
|
5018
|
+
"[{\"no\": 1, \"title\": \"卷名\", \"summary\": \"卷定位与剧情重心\", \"chapterStart\": 1, \"chapterEnd\": 80}]",
|
|
5019
|
+
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。",
|
|
5020
|
+
"重要:直接输出 JSON 结果本身,不要把思考过程或推理内容写在输出里。",
|
|
5021
|
+
renderLengthContract([["卷名", STRUCTURED_FIELD_LIMITS.chapterTitle], ["每卷定位", STRUCTURED_FIELD_LIMITS.chapterSummary]])
|
|
5022
|
+
].join("\n");
|
|
5023
|
+
}
|
|
5024
|
+
/** The chapter-planning prompt template. */
|
|
5025
|
+
function planSystemPrompt(volumes) {
|
|
5026
|
+
return [
|
|
5027
|
+
"你是一位资深中文网文策划编辑,擅长把小说大纲拆解为可执行的章节计划。",
|
|
5028
|
+
"你会收到一份小说大纲。请根据大纲的设定、主线与节奏,规划出一份章节计划。",
|
|
5029
|
+
"要求:",
|
|
5030
|
+
"1. 每章必须有明确的核心剧情推进(不能只是过渡或凑字数)。",
|
|
5031
|
+
"2. 章节之间要衔接自然,前章结尾为后章埋下钩子。",
|
|
5032
|
+
"3. 严格遵循大纲的人设、金手指规则、战力体系与世界观设定,不得自行发明冲突设定。",
|
|
5033
|
+
"4. 输出必须是合法的 JSON 数组,不要输出任何其他文字或 Markdown 代码块标记。",
|
|
5034
|
+
"5. 数组每个元素格式:{\"title\": \"章节标题(10字以内,有网文感)\", \"beats\": \"结构化剧情要点(150-250字,必须包含四段,段间用换行分隔):\\n本章目标:本章要完成的核心推进;\\n剧情要点:主要情节的起承转合(2-4 句);\\n爽点/钩子:本章的爽点兑现或情绪钩子;\\n结尾钩子:本章结尾为下一章埋下的悬念\"}",
|
|
5035
|
+
"6. 每个章节对象可额外包含以下可选字段(尽量给出,缺失则跳过):tension(0-100 整数:本章张力目标,对抗/揭示/抉择/代价的强度;铺垫与日常章明显偏低,与相邻章节形成起伏)、mustAdvance(数组,本章必须推进的局面/关系/信息/风险/决策变化);mustPreserve(数组,本章必须保持不破坏的项);characterHardFacts(数组,本章不可违背的人物硬事实:身份/阵营/境界/当前位置/知情度);endingHook(字符串,本章结尾钩子要求);obligation(字符串,本章义务合约一句话)。",
|
|
5036
|
+
"重要:beats 字段内部必须使用 \\n 转义表示换行(JSON 字符串内不得有真实换行符),其余字符串值也不得包含真实换行符,JSON 必须在一段内完整结束。",
|
|
5037
|
+
"重要:直接输出 JSON 结果本身,不要把思考过程或推理内容写在输出里。",
|
|
5038
|
+
volumes !== void 0 && volumes.length > 0 ? ["\n全书分卷结构(规划章节时需落在对应卷内):"].concat(volumes.map((v) => `第${v.no}卷《${v.title}》:${v.summary}(章节 ${v.chapterStart}-${v.chapterEnd})` + (v.strategy !== void 0 && v.strategy !== "" ? `\n 卷战略:${v.strategy}` : "") + (v.pacing !== void 0 && v.pacing !== "" ? `\n 卷节奏板:${v.pacing}` : ""))).join("\n") : ""
|
|
5039
|
+
].join("\n");
|
|
5040
|
+
}
|
|
5041
|
+
/**
|
|
5042
|
+
* 续写规划的附加要求(拼在 planSystemPrompt 之后)。
|
|
5043
|
+
*
|
|
5044
|
+
* 与 user 侧的「续写硬性要求」分工:这里管**标题与接续的唯一性**,
|
|
5045
|
+
* user 侧管「已发生事件不得重写」与全书进度感知。
|
|
5046
|
+
*/
|
|
5047
|
+
function planContinuationSuffix() {
|
|
5048
|
+
return "\n重要:本次是**续写规划**——已有章节的剧情不得重写或重复,新章节标题不得与已有章节标题相同,新章节的剧情必须从上一章结尾自然接续(人物状态、时间线、地点衔接一致)。";
|
|
5049
|
+
}
|
|
5050
|
+
/** 自动导演:整本编排/节奏/风险/再平衡建议(不输出正文)。 */
|
|
5051
|
+
function directorSystemPrompt() {
|
|
5052
|
+
return "你是一位长篇网文自动导演,负责整本编排、节奏、风险与再平衡。只给出可执行建议,不输出正文。";
|
|
5053
|
+
}
|
|
5054
|
+
//#endregion
|
|
5055
|
+
//#region src/prompt-cache.ts
|
|
5056
|
+
/**
|
|
5057
|
+
* 分层顺序。写死不开放配置——「哪层在前」是缓存语义决定的,不是产品偏好。
|
|
5058
|
+
*/
|
|
5059
|
+
const LAYER_ORDER = [
|
|
5060
|
+
"fixed",
|
|
5061
|
+
"book",
|
|
5062
|
+
"chapter"
|
|
5063
|
+
];
|
|
5064
|
+
/**
|
|
5065
|
+
* 按「固定 → 本书 → 本章」组装系统提示词。
|
|
5066
|
+
*
|
|
5067
|
+
* 同层内保持书写顺序(稳定排序);空白块丢弃,不在结果里留空段。
|
|
5068
|
+
* @param blocks 调用方按书写顺序给出的块(层标可以乱,组装后一定分层有序)。
|
|
5069
|
+
* @param separator 块间分隔符(默认换行)。
|
|
5070
|
+
*/
|
|
5071
|
+
function assemblePromptBlocks(blocks, separator = "\n") {
|
|
5072
|
+
const rank = (layer) => LAYER_ORDER.indexOf(layer);
|
|
5073
|
+
return blocks.map((block, index) => ({
|
|
5074
|
+
block,
|
|
5075
|
+
index
|
|
5076
|
+
})).filter((entry) => entry.block.text.trim() !== "").sort((a, b) => rank(a.block.layer) - rank(b.block.layer) || a.index - b.index).map((entry) => entry.block.text).join(separator);
|
|
5077
|
+
}
|
|
5078
|
+
//#endregion
|
|
5079
|
+
//#region src/prompt-slots.ts
|
|
5080
|
+
/** 允许作者编辑的槽位(白名单;不在此列表的提示词一律不可改)。 */
|
|
5081
|
+
const PROMPT_SLOTS = [
|
|
5082
|
+
{
|
|
5083
|
+
id: "styleExtra",
|
|
5084
|
+
label: "文风补充",
|
|
5085
|
+
hint: "追加到写作提示词末尾的风格偏好(句式、叙述习惯、用词倾向)。",
|
|
5086
|
+
placeholder: "例如:多用短句和对话推进,少用成语;环境描写只写能影响动作的部分。",
|
|
5087
|
+
maxChars: 1200
|
|
5088
|
+
},
|
|
5089
|
+
{
|
|
5090
|
+
id: "antiAiExtra",
|
|
5091
|
+
label: "反 AI 补充",
|
|
5092
|
+
hint: "本书额外的\"别这么写\"清单,与内置反 AI 规则叠加(不能覆盖内置规则)。",
|
|
5093
|
+
placeholder: "例如:不要每段都用比喻;不要用\"空气中弥漫着\"这类句式。",
|
|
5094
|
+
maxChars: 1200
|
|
5095
|
+
},
|
|
5096
|
+
{
|
|
5097
|
+
id: "dialogueExtra",
|
|
5098
|
+
label: "对话补充",
|
|
5099
|
+
hint: "角色说话方式的额外约束,写入时会与角色卡一起注入。",
|
|
5100
|
+
placeholder: "例如:长辈说话带方言词;主角对熟人用绰号称呼。",
|
|
5101
|
+
maxChars: 800
|
|
5102
|
+
},
|
|
5103
|
+
{
|
|
5104
|
+
id: "endingHookExtra",
|
|
5105
|
+
label: "章末钩子偏好",
|
|
5106
|
+
hint: "对结尾钩子的额外要求(与章节计划里的 endingHook 叠加)。",
|
|
5107
|
+
placeholder: "例如:结尾尽量落在一个未闭合的选择上,而不是新出现的敌人。",
|
|
5108
|
+
maxChars: 600
|
|
5109
|
+
}
|
|
5110
|
+
];
|
|
5111
|
+
/** 槽位 id → 规格。 */
|
|
5112
|
+
const SLOT_BY_ID = new Map(PROMPT_SLOTS.map((s) => [s.id, s]));
|
|
5113
|
+
/**
|
|
5114
|
+
* 读取并规整项目里的槽位内容(去空白、按上限截断)。
|
|
5115
|
+
*
|
|
5116
|
+
* @param project 项目状态。
|
|
5117
|
+
* @returns 非空槽位列表(保持 PROMPT_SLOTS 的声明顺序)。
|
|
5118
|
+
*/
|
|
5119
|
+
function readPromptSlots(project) {
|
|
5120
|
+
const stored = project.promptSlots ?? {};
|
|
5121
|
+
const out = [];
|
|
5122
|
+
for (const spec of PROMPT_SLOTS) {
|
|
5123
|
+
const raw = (stored[spec.id] ?? "").trim();
|
|
5124
|
+
if (raw === "") continue;
|
|
5125
|
+
const truncated = raw.length > spec.maxChars;
|
|
5126
|
+
out.push({
|
|
5127
|
+
id: spec.id,
|
|
5128
|
+
label: spec.label,
|
|
5129
|
+
text: truncated ? raw.slice(0, spec.maxChars) : raw,
|
|
5130
|
+
truncated,
|
|
5131
|
+
chars: raw.length
|
|
5132
|
+
});
|
|
5133
|
+
}
|
|
5134
|
+
return out;
|
|
5135
|
+
}
|
|
5136
|
+
/**
|
|
5137
|
+
* 把槽位渲染成注入提示词的段落。
|
|
5138
|
+
*
|
|
5139
|
+
* @param project 项目状态。
|
|
5140
|
+
* @returns 提示词片段;无槽位时返回空串。
|
|
5141
|
+
*/
|
|
5142
|
+
function renderPromptSlots(project) {
|
|
5143
|
+
const slots = readPromptSlots(project);
|
|
5144
|
+
if (slots.length === 0) return "";
|
|
5145
|
+
const lines = ["==================== 作者偏好(本书自定义,优先级低于道藏/红线/合规) ===================="];
|
|
5146
|
+
for (const slot of slots) lines.push(`【${slot.label}】${slot.text}${slot.truncated ? `(已按 ${SLOT_BY_ID.get(slot.id)?.maxChars ?? 0} 字上限截断,原 ${slot.chars} 字)` : ""}`);
|
|
5147
|
+
lines.push("以上偏好只影响表达方式;与道藏、写作红线、内容合规红线冲突时,一律以后者为准。");
|
|
5148
|
+
return lines.join("\n");
|
|
5149
|
+
}
|
|
5150
|
+
/**
|
|
5151
|
+
* 校验并规整一次槽位写入。
|
|
5152
|
+
*
|
|
5153
|
+
* @param id 槽位 id。
|
|
5154
|
+
* @param value 内容(空串表示清空该槽位)。
|
|
5155
|
+
* @returns 归一化后的内容。
|
|
5156
|
+
* @throws 槽位 id 不在白名单时抛错。
|
|
5157
|
+
*/
|
|
5158
|
+
function normalizeSlotValue(id, value) {
|
|
5159
|
+
if (SLOT_BY_ID.get(id) === void 0) throw new Error(`未知提示词槽位:${id}`);
|
|
5160
|
+
return value.replace(/\r\n/g, "\n").trim();
|
|
5161
|
+
}
|
|
5162
|
+
//#endregion
|
|
5163
|
+
//#region src/prompting.ts
|
|
5164
|
+
/** 章节写作约束:把写作骨架渲染成可附加到系统提示词末尾的必达/禁止/自查块。 */
|
|
5165
|
+
function renderChapterWriterSkeleton(meta) {
|
|
5166
|
+
const pov = meta.pov ?? "第三人称有限视角,严格跟随主角所见所知。";
|
|
5167
|
+
const tone = meta.tonePreference ?? "文风贴合本书设定,用具体细节与动作推进。";
|
|
5168
|
+
const hook = meta.endingHookPreference ?? "章末留一个明确的钩子(新信息、新风险或未闭合的选择)。";
|
|
5169
|
+
const antiAi = meta.antiAiRules ?? "";
|
|
5170
|
+
return [
|
|
5171
|
+
"==================== 章节生成骨架(必达 / 禁止 / 输出前自查) ====================",
|
|
5172
|
+
"【叙事视角】" + pov,
|
|
5173
|
+
"【本章必达】本章必须发生实质变化——局面、关系、信息、风险、决策至少动一项;开篇直接进入情境,不复述上一章。",
|
|
5174
|
+
"【禁止事项】不写以总结、复盘、解释为主的段落;不新增核心角色,不写入与上下文冲突的设定;不为凑字数注水。",
|
|
5175
|
+
"【篇幅】目标 " + meta.targetChars + " 字,区间 " + meta.minChars + "-" + meta.maxChars + " 字;不够就继续推进有效情节,不许草草收尾。",
|
|
5176
|
+
"【结尾】" + hook,
|
|
5177
|
+
"【表达】用具体动作、对话与可感知细节往前推;" + tone,
|
|
5178
|
+
"【反 AI】" + antiAi,
|
|
5179
|
+
"【输出前自查】逐项确认:读者的回报、关键转折、章末净变化是否看得见?上一章留下的钩子有没有交代?人物的硬事实有没有被写反?确认无误再输出,正文里不要出现核查过程。"
|
|
5180
|
+
].join("\n");
|
|
5181
|
+
}
|
|
5182
|
+
/**
|
|
5183
|
+
* 本项目设计的参考曲线形状(0-100 的采样序列)。
|
|
5184
|
+
* 只描述"形状",不含任何外部数值表。
|
|
5185
|
+
*/
|
|
5186
|
+
const REFERENCE_SHAPES = {
|
|
5187
|
+
escalation: [
|
|
5188
|
+
20,
|
|
5189
|
+
26,
|
|
5190
|
+
34,
|
|
5191
|
+
32,
|
|
5192
|
+
44,
|
|
5193
|
+
52,
|
|
5194
|
+
48,
|
|
5195
|
+
62,
|
|
5196
|
+
70,
|
|
5197
|
+
66,
|
|
5198
|
+
82,
|
|
5199
|
+
92
|
|
5200
|
+
],
|
|
5201
|
+
suspense: [
|
|
5202
|
+
30,
|
|
5203
|
+
42,
|
|
5204
|
+
38,
|
|
5205
|
+
50,
|
|
5206
|
+
46,
|
|
5207
|
+
58,
|
|
5208
|
+
62,
|
|
5209
|
+
56,
|
|
5210
|
+
70,
|
|
5211
|
+
78,
|
|
5212
|
+
86,
|
|
5213
|
+
74
|
|
5214
|
+
],
|
|
5215
|
+
wave: [
|
|
5216
|
+
45,
|
|
5217
|
+
30,
|
|
5218
|
+
55,
|
|
5219
|
+
34,
|
|
5220
|
+
62,
|
|
5221
|
+
40,
|
|
5222
|
+
70,
|
|
5223
|
+
46,
|
|
5224
|
+
78,
|
|
5225
|
+
52,
|
|
5226
|
+
84,
|
|
5227
|
+
60
|
|
5228
|
+
],
|
|
5229
|
+
frontLoad: [
|
|
5230
|
+
78,
|
|
5231
|
+
66,
|
|
5232
|
+
58,
|
|
5233
|
+
50,
|
|
5234
|
+
46,
|
|
5235
|
+
44,
|
|
5236
|
+
42,
|
|
5237
|
+
44,
|
|
5238
|
+
46,
|
|
5239
|
+
48,
|
|
5240
|
+
50,
|
|
5241
|
+
52
|
|
5242
|
+
],
|
|
5243
|
+
flat: [
|
|
5244
|
+
40,
|
|
5245
|
+
42,
|
|
5246
|
+
44,
|
|
5247
|
+
42,
|
|
5248
|
+
46,
|
|
5249
|
+
44,
|
|
5250
|
+
48,
|
|
5251
|
+
46,
|
|
5252
|
+
50,
|
|
5253
|
+
48,
|
|
5254
|
+
52,
|
|
5255
|
+
50
|
|
5256
|
+
]
|
|
5257
|
+
};
|
|
5258
|
+
/** 参考曲线的中文标签(面板展示)。 */
|
|
5259
|
+
const TENSION_PRESET_LABELS = {
|
|
5260
|
+
escalation: "递进爬升",
|
|
5261
|
+
suspense: "悬疑加压",
|
|
5262
|
+
wave: "波浪呼吸",
|
|
5263
|
+
frontLoad: "前紧后稳",
|
|
5264
|
+
flat: "平稳推进",
|
|
5265
|
+
custom: "自定义"
|
|
5266
|
+
};
|
|
5267
|
+
/** 全部可选预设(UI 下拉用)。 */
|
|
5268
|
+
const TENSION_PRESETS = [
|
|
5269
|
+
"escalation",
|
|
5270
|
+
"suspense",
|
|
5271
|
+
"wave",
|
|
5272
|
+
"frontLoad",
|
|
5273
|
+
"flat",
|
|
5274
|
+
"custom"
|
|
5275
|
+
];
|
|
5276
|
+
/** 把参考形状按章节数重采样(最近邻,保持形状不引入平滑失真)。 */
|
|
5277
|
+
function referenceCurve(preset, chapterCount) {
|
|
5278
|
+
if (chapterCount <= 0) return [];
|
|
5279
|
+
if (preset === "custom") return [];
|
|
5280
|
+
const shape = REFERENCE_SHAPES[preset];
|
|
5281
|
+
if (chapterCount === 1) return [shape[Math.floor(shape.length / 2)] ?? 50];
|
|
5282
|
+
return Array.from({ length: chapterCount }, (_, i) => {
|
|
5283
|
+
const pos = i / (chapterCount - 1) * (shape.length - 1);
|
|
5284
|
+
const lo = Math.floor(pos);
|
|
5285
|
+
const hi = Math.min(shape.length - 1, lo + 1);
|
|
5286
|
+
const t = pos - lo;
|
|
5287
|
+
const a = shape[lo] ?? 50;
|
|
5288
|
+
const b = shape[hi] ?? a;
|
|
5289
|
+
return Math.round(a + (b - a) * t);
|
|
5290
|
+
});
|
|
5291
|
+
}
|
|
5292
|
+
/** 读一章的目标张力(未设置时返回 undefined)。 */
|
|
5293
|
+
function targetTension(chapter) {
|
|
5294
|
+
return typeof chapter.tension === "number" ? clampTension(chapter.tension) : void 0;
|
|
5295
|
+
}
|
|
5296
|
+
/** 读一章的实际张力(审稿时模型打分,未设置时返回 undefined)。 */
|
|
5297
|
+
function actualTension(chapter) {
|
|
5298
|
+
const fromReview = chapter.review?.tension;
|
|
5299
|
+
if (typeof fromReview === "number") return clampTension(fromReview);
|
|
5300
|
+
return typeof chapter.tensionActual === "number" ? clampTension(chapter.tensionActual) : void 0;
|
|
5301
|
+
}
|
|
5302
|
+
/** 收敛到合法区间并取整。 */
|
|
5303
|
+
function clampTension(value) {
|
|
5304
|
+
if (!Number.isFinite(value)) return 0;
|
|
5305
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
5306
|
+
}
|
|
5307
|
+
/**
|
|
5308
|
+
* 渲染注入写作提示词的「本章张力」块。
|
|
5309
|
+
*
|
|
5310
|
+
* @param project 项目状态。
|
|
5311
|
+
* @param chapterNo 本章章号。
|
|
5312
|
+
* @returns 提示词片段;没有张力信息时返回空串。
|
|
5313
|
+
*/
|
|
5314
|
+
function renderTensionBlock(project, chapterNo) {
|
|
5315
|
+
const preset = project.tensionCurve?.preset;
|
|
5316
|
+
const chapters = [...project.chapters ?? []].sort((a, b) => a.no - b.no);
|
|
5317
|
+
const idx = chapters.findIndex((c) => c.no === chapterNo);
|
|
5318
|
+
if (idx === -1) return "";
|
|
5319
|
+
const target = targetTension(chapters[idx]);
|
|
5320
|
+
const prev = idx > 0 ? targetTension(chapters[idx - 1]) : void 0;
|
|
5321
|
+
if (target === void 0 && prev === void 0) return "";
|
|
5322
|
+
const ref = preset !== void 0 && preset !== "custom" ? referenceCurve(preset, chapters.length)[idx] : void 0;
|
|
5323
|
+
const parts = [];
|
|
5324
|
+
if (target !== void 0) parts.push(`本章目标张力 ${target}/100`);
|
|
5325
|
+
if (prev !== void 0 && target !== void 0) {
|
|
5326
|
+
const delta = target - prev;
|
|
5327
|
+
parts.push(delta >= 10 ? "比上一章明显更紧" : delta <= -10 ? "比上一章明显放松(给读者呼吸口)" : "与上一章持平");
|
|
5328
|
+
}
|
|
5329
|
+
if (ref !== void 0) parts.push(`参考曲线(${TENSION_PRESET_LABELS[preset ?? "custom"]})在此处约为 ${ref}`);
|
|
5330
|
+
if (parts.length === 0) return "";
|
|
5331
|
+
return [
|
|
5332
|
+
"==================== 本章张力(写作时把握松紧) ====================",
|
|
5333
|
+
parts.join(";") + "。",
|
|
5334
|
+
"低张力段用于铺垫关系、交代信息、留呼吸;高张力段用于对抗、揭示、抉择与代价。",
|
|
5335
|
+
"同一章内可以有起伏,但整章的平均松紧要贴近目标值;不要连续多章都停在同一个紧张度。"
|
|
5336
|
+
].join("\n");
|
|
5337
|
+
}
|
|
5338
|
+
/**
|
|
5339
|
+
* 规则核对张力曲线(不调模型)。
|
|
5340
|
+
*
|
|
5341
|
+
* 检查四类曲线级问题:
|
|
5342
|
+
* 1) 实际与目标偏差过大(>25);
|
|
5343
|
+
* 2) 连续多章几乎同值(≥4 章落差 <6,平台期无起伏);
|
|
5344
|
+
* 3) 高点之后没有回落(连续 ≥3 章处在高位 ≥80);
|
|
5345
|
+
* 4) 长期低位(连续 ≥5 章 <40,容易拖沓)。
|
|
5346
|
+
*
|
|
5347
|
+
* @param project 项目状态。
|
|
5348
|
+
* @returns 问题清单(可能为空)。
|
|
5349
|
+
*/
|
|
5350
|
+
/**
|
|
5351
|
+
* run 型问题(连续同值 / 连续高位 / 连续低位)的受影响章号。
|
|
5352
|
+
* 返回**完整区间**而不是首尾两点:中间章同样受这条曲线问题影响,
|
|
5353
|
+
* 面板要能在每一章上给出「一键按建议修订」,合并层也要能把建议落到中间章。
|
|
5354
|
+
*/
|
|
5355
|
+
function runChapters(from, to) {
|
|
5356
|
+
const out = [];
|
|
5357
|
+
for (let no = from; no <= to; no++) out.push(no);
|
|
5358
|
+
return out;
|
|
5359
|
+
}
|
|
5360
|
+
function detectTensionIssues(project) {
|
|
5361
|
+
const values = [...project.chapters ?? []].sort((a, b) => a.no - b.no).map((c) => ({
|
|
5362
|
+
no: c.no,
|
|
5363
|
+
target: targetTension(c),
|
|
5364
|
+
actual: actualTension(c)
|
|
5365
|
+
}));
|
|
5366
|
+
if (values.filter((v) => v.target !== void 0 || v.actual !== void 0).length < 3) return [];
|
|
5367
|
+
const issues = [];
|
|
5368
|
+
for (const v of values) {
|
|
5369
|
+
if (v.target === void 0 || v.actual === void 0) continue;
|
|
5370
|
+
const gap = v.actual - v.target;
|
|
5371
|
+
if (Math.abs(gap) > 25) issues.push({
|
|
5372
|
+
severity: Math.abs(gap) > 40 ? "high" : "medium",
|
|
5373
|
+
chapters: [v.no],
|
|
5374
|
+
item: `第${v.no}章张力偏差较大:目标 ${v.target},实际 ${v.actual}(${gap > 0 ? "比目标更紧" : "比目标更松"})`,
|
|
5375
|
+
suggestion: gap > 0 ? "本章对抗/揭示密度偏高,考虑把部分冲突后移,留出呼吸段。" : "本章推进偏平,考虑加一处对抗、揭示或代价。"
|
|
5376
|
+
});
|
|
5377
|
+
}
|
|
5378
|
+
let run = 1;
|
|
5379
|
+
for (let i = 1; i < values.length; i++) {
|
|
5380
|
+
const a = values[i - 1].target ?? values[i - 1].actual;
|
|
5381
|
+
const b = values[i].target ?? values[i].actual;
|
|
5382
|
+
if (a !== void 0 && b !== void 0 && Math.abs(b - a) < 6) {
|
|
5383
|
+
run++;
|
|
5384
|
+
if (run === 4) {
|
|
5385
|
+
const from = values[i - run + 1].no;
|
|
5386
|
+
issues.push({
|
|
5387
|
+
severity: "medium",
|
|
5388
|
+
chapters: runChapters(from, values[i].no),
|
|
5389
|
+
item: `第${from}-${values[i].no}章张力几乎无变化(连续 ${run} 章)`,
|
|
5390
|
+
suggestion: "安排一次明显抬升或一次释放,让曲线有起伏,避免读者疲劳。"
|
|
5391
|
+
});
|
|
5392
|
+
}
|
|
5393
|
+
} else run = 1;
|
|
5394
|
+
}
|
|
5395
|
+
let highRun = 0;
|
|
5396
|
+
for (const v of values) {
|
|
5397
|
+
const val = v.target ?? v.actual;
|
|
5398
|
+
if (val !== void 0 && val >= 80) {
|
|
5399
|
+
highRun++;
|
|
5400
|
+
if (highRun === 3) issues.push({
|
|
5401
|
+
severity: "medium",
|
|
5402
|
+
chapters: runChapters(v.no - 2, v.no),
|
|
5403
|
+
item: `连续 ${highRun} 章处在高位(≥80),缺少呼吸口`,
|
|
5404
|
+
suggestion: "高潮之后补一章缓冲(关系推进、信息消化或日常),再进入下一轮对抗。"
|
|
5405
|
+
});
|
|
5406
|
+
} else highRun = 0;
|
|
5407
|
+
}
|
|
5408
|
+
let lowRun = 0;
|
|
5409
|
+
for (const v of values) {
|
|
5410
|
+
const val = v.target ?? v.actual;
|
|
5411
|
+
if (val !== void 0 && val < 40) {
|
|
5412
|
+
lowRun++;
|
|
5413
|
+
if (lowRun === 5) issues.push({
|
|
5414
|
+
severity: "high",
|
|
5415
|
+
chapters: runChapters(v.no - 4, v.no),
|
|
5416
|
+
item: `连续 ${lowRun} 章张力低于 40,推进可能偏慢`,
|
|
5417
|
+
suggestion: "提前兑现一个目标、抛出一个新风险,或让配角线产生冲撞。"
|
|
5418
|
+
});
|
|
5419
|
+
} else lowRun = 0;
|
|
5420
|
+
}
|
|
5421
|
+
return issues;
|
|
5422
|
+
}
|
|
5423
|
+
/**
|
|
5424
|
+
* 组装面板需要的曲线数据(章节号 / 目标 / 实际 / 参考)。
|
|
5425
|
+
*
|
|
5426
|
+
* @param project 项目状态。
|
|
5427
|
+
* @returns 逐章数据点与预设名。
|
|
5428
|
+
*/
|
|
5429
|
+
function buildCurveData(project) {
|
|
5430
|
+
const preset = project.tensionCurve?.preset ?? "escalation";
|
|
5431
|
+
const chapters = [...project.chapters ?? []].sort((a, b) => a.no - b.no);
|
|
5432
|
+
const custom = project.tensionCurve?.custom ?? [];
|
|
5433
|
+
const reference = preset === "custom" ? custom : referenceCurve(preset, chapters.length);
|
|
5434
|
+
return {
|
|
5435
|
+
preset,
|
|
5436
|
+
points: chapters.map((c, i) => {
|
|
5437
|
+
const point = {
|
|
5438
|
+
no: c.no,
|
|
5439
|
+
title: c.title
|
|
5440
|
+
};
|
|
5441
|
+
const t = targetTension(c);
|
|
5442
|
+
const a = actualTension(c);
|
|
5443
|
+
if (t !== void 0) point.target = t;
|
|
5444
|
+
if (a !== void 0) point.actual = a;
|
|
5445
|
+
if (reference[i] !== void 0) point.reference = reference[i];
|
|
5446
|
+
return point;
|
|
5447
|
+
})
|
|
5448
|
+
};
|
|
5449
|
+
}
|
|
5450
|
+
//#endregion
|
|
5451
|
+
//#region src/prompts/common.ts
|
|
5452
|
+
/**
|
|
5453
|
+
* 提示词共享件:内容合规红线。
|
|
5454
|
+
*
|
|
5455
|
+
* 所有书籍、所有章节无条件生效,优先级高于单书大纲/道藏中的任何设定与作者
|
|
5456
|
+
* 自定义红线。原来住在 engine.ts 里,随二期拆分成域模块后迁到此处——
|
|
5457
|
+
* 各域模块(writing/review/polish…)都注入它,放共享件里避免循环导入。
|
|
5458
|
+
*/
|
|
5459
|
+
/** 内容合规红线(平台硬性要求)。注入点:章节生成 + 审稿 + 润色 + 局部修订系统提示(命中即 high)。 */
|
|
5460
|
+
const COMPLIANCE_REDLINES = [
|
|
5461
|
+
"1. 不得出现反对宪法所确定的基本原则的内容。",
|
|
5462
|
+
"2. 不得出现危害国家安全、泄露国家秘密、颠覆国家政权、破坏国家统一的内容。",
|
|
5463
|
+
"3. 不得出现危害国家荣誉和利益的内容。",
|
|
5464
|
+
"4. 不得出现煽动民族仇恨、民族歧视、破坏民族团结的内容。",
|
|
5465
|
+
"5. 不得出现破坏国家宗教政策、宣扬邪教和愚昧迷信的内容(不得以真实宗教、邪教或迷信活动为背景进行宣扬)。",
|
|
5466
|
+
"6. 不得出现散布谣言、扰乱社会秩序、破坏社会稳定的内容。",
|
|
5467
|
+
"7. 不得出现淫秽色情、赌博、暴力、凶杀、恐怖或教唆犯罪的内容(网文语境:禁止露骨性描写、血腥暴力渲染、赌博教唆、犯罪手法详细教学)。",
|
|
5468
|
+
"8. 不得出现侮辱或者诽谤他人、侵害他人合法权益的内容(不得以真实人物、组织为原型进行侮辱或影射攻击)。",
|
|
5469
|
+
"9. 不得出现法律法规禁止的其他内容。"
|
|
5470
|
+
];
|
|
5471
|
+
//#endregion
|
|
5472
|
+
//#region src/prompts/world.ts
|
|
5473
|
+
/** 把大世界结构化数据渲染成提示词块(境界体系按顺序强约束)。 */
|
|
5474
|
+
function renderWorld(world) {
|
|
5475
|
+
if (world === void 0) return "";
|
|
5476
|
+
const sections = ["==================== 大世界(结构化设定,写作时严格遵守) ===================="];
|
|
5477
|
+
if (world.realms.length > 0) {
|
|
5478
|
+
sections.push("境界体系(由低到高,不得随意跳级或自创境界):");
|
|
5479
|
+
world.realms.forEach((realm, i) => {
|
|
5480
|
+
sections.push(`${i + 1}. ${realm.name}${realm.description !== "" ? ` — ${realm.description}` : ""}`);
|
|
5481
|
+
});
|
|
5482
|
+
}
|
|
5483
|
+
if (world.regions.length > 0) {
|
|
5484
|
+
sections.push("地理区域:");
|
|
5485
|
+
for (const region of world.regions) sections.push(`- ${region.name}${region.description !== "" ? `:${region.description}` : ""}${region.faction !== void 0 && region.faction !== "" ? `(势力:${region.faction})` : ""}`);
|
|
5486
|
+
}
|
|
5487
|
+
if (world.factions.length > 0) {
|
|
5488
|
+
sections.push("势力分布:");
|
|
5489
|
+
for (const faction of world.factions) sections.push(`- ${faction.name}(${faction.kind})${faction.description !== "" ? `:${faction.description}` : ""}${faction.region !== void 0 && faction.region !== "" ? `(驻地:${faction.region})` : ""}`);
|
|
5490
|
+
}
|
|
5491
|
+
return sections.join("\n");
|
|
5492
|
+
}
|
|
5493
|
+
/** 大世界提炼:从大纲 + 道藏生成结构化境界体系/区域/势力。 */
|
|
5494
|
+
function extractWorldSystemPrompt() {
|
|
5495
|
+
return [
|
|
5496
|
+
"你是一位网文世界观架构师。请根据小说大纲与道藏,提炼结构化「大世界」数据。",
|
|
5497
|
+
"输出必须是合法 JSON 对象:",
|
|
5498
|
+
"{\"realms\": [{\"name\": \"境界名\", \"description\": \"突破条件/寿命/标志等\"}], \"regions\": [{\"name\": \"区域名\", \"description\": \"描述\", \"faction\": \"关联势力名或空\"}], \"factions\": [{\"name\": \"势力名\", \"kind\": \"宗门/家族/王朝/组织等\", \"description\": \"描述\", \"region\": \"驻地区域或空\"}]}",
|
|
5499
|
+
"要求:",
|
|
5500
|
+
"1. realms 按由低到高顺序排列(修仙题材必须含完整境界链;无境界设定的题材可输出空数组)。",
|
|
5501
|
+
"2. 数量贴合大纲:realms 3-12 个,regions 2-10 个,factions 2-10 个。",
|
|
5502
|
+
"3. 内容严格来自大纲与道藏,不要凭空发明与大纲冲突的设定。",
|
|
5503
|
+
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。"
|
|
5504
|
+
].join("\n");
|
|
5505
|
+
}
|
|
5506
|
+
//#endregion
|
|
5507
|
+
//#region src/prompts/writing.ts
|
|
5508
|
+
/** Build the writing system prompt (bible + outline + active foreshadows).
|
|
5509
|
+
* `targetChars` 来自每章计划(规划时快照,= 设置的每章目标字数);无则退回默认 3500。
|
|
5510
|
+
* 字数区间按目标动态生成(±15%,取整到百位),避免系统提示词与设置互相冲突。
|
|
5511
|
+
* `lengthRule` 可覆盖第 1 条字数要求(整章修订/改编时按原文长度为准)。 */
|
|
5512
|
+
function writeSystemPrompt(project, targetChars, lengthRule, beforeChapter) {
|
|
5513
|
+
const bible = project.bible;
|
|
5514
|
+
const blocks = [];
|
|
5515
|
+
const target = targetChars !== void 0 && targetChars > 0 ? targetChars : 3500;
|
|
5516
|
+
const lo = Math.max(1e3, Math.round(target * .85 / 100) * 100);
|
|
5517
|
+
const hi = Math.max(lo + 100, Math.round(target * 1.15 / 100) * 100);
|
|
5518
|
+
const rules = ["写作硬性要求:"];
|
|
5519
|
+
rules.push(lengthRule ?? `1. 每章 ${lo}-${hi} 字(目标 ${target} 字,按中文字符计),只输出章节正文,不要输出标题、章回名、作者的话或任何 Markdown 标记。`);
|
|
5520
|
+
rules.push("2. 以主角视角展开,动作、对话、心理描写交替推进,禁止大段设定说明。");
|
|
5521
|
+
rules.push("3. 尊重大纲与道藏:人设不崩、金手指规则不自相矛盾、战力不随意膨胀。");
|
|
5522
|
+
rules.push("4. 章末留一个钩子(悬念、反转或新线索),吸引读者读下一章。");
|
|
5523
|
+
rules.push("5. 语言流畅自然,符合中文网文语感,避免翻译腔与病句。");
|
|
5524
|
+
rules.push("6. 对话与冲突密度:每章至少 1 处实质对话或正面对抗/交锋场面;推理与心理活动必须用动作、环境细节、微表情、对话呈现,禁止整章纯内心独白铺陈(禁止\"解说式\"交代线索)。");
|
|
5525
|
+
rules.push("7. 反派与对手的行动力:本章出现的反派/对手必须有其行动、反制或压迫感(布局、试探、追索、交锋至少占其一),不得作为纯背景板存在。");
|
|
5526
|
+
rules.push("8. 配角辨识度:重要新登场配角应给姓名或可辨识的独有特征;禁止通篇用\"瘦高个/灰衣人/戴面具者\"等身形标签代称同一角色。");
|
|
5527
|
+
rules.push("9. 信息呈现方式:关键线索、设定、局势通过对话、动作、发现物呈现,禁止主角内心\"讲解\"给读者听。");
|
|
5528
|
+
blocks.push({
|
|
5529
|
+
layer: "fixed",
|
|
5530
|
+
text: rules.join("\n")
|
|
5531
|
+
});
|
|
5532
|
+
blocks.push({
|
|
5533
|
+
layer: "fixed",
|
|
5534
|
+
text: [
|
|
5535
|
+
"==================== 内容合规红线(平台硬性要求,最高优先级,违反即失败) ====================",
|
|
5536
|
+
COMPLIANCE_REDLINES.join("\n"),
|
|
5537
|
+
"以上九条为硬性底线,任何情况下不得以任何形式出现或影射;若剧情确需涉及(如批判、反讽),只能以明确否定、揭露、批判的立场呈现,且不得展开细节。"
|
|
5538
|
+
].join("\n")
|
|
5539
|
+
});
|
|
5540
|
+
const book = [];
|
|
5541
|
+
if (bible !== void 0) {
|
|
5542
|
+
book.push("==================== 道藏(写作时严格遵守) ====================");
|
|
5543
|
+
if (bible.genre !== "") book.push(`题材基调:${bible.genre}`);
|
|
5544
|
+
if (bible.worldRules.length > 0) book.push("世界规则:\n" + bible.worldRules.map((r) => `- ${r}`).join("\n"));
|
|
5545
|
+
const roleLib = project.roles ?? [];
|
|
5546
|
+
const labelName = {
|
|
5547
|
+
protagonist: "主角",
|
|
5548
|
+
female_lead: "女主",
|
|
5549
|
+
female_support: "女配",
|
|
5550
|
+
support: "配角",
|
|
5551
|
+
antagonist: "反派",
|
|
5552
|
+
extra: "路人"
|
|
5553
|
+
};
|
|
5554
|
+
const seenRole = /* @__PURE__ */ new Set();
|
|
5555
|
+
const mergedRoles = [];
|
|
5556
|
+
for (const r of roleLib) {
|
|
5557
|
+
seenRole.add(r.name);
|
|
5558
|
+
const card = bible.characters.find((c) => c.name === r.name);
|
|
5559
|
+
const traits = card !== void 0 ? card.traits : Array.isArray(r.traits) ? r.traits : [];
|
|
5560
|
+
const goals = card !== void 0 && card.goals !== "" ? card.goals : r.goals;
|
|
5561
|
+
const relations = card !== void 0 && card.relations !== "" ? card.relations : Array.isArray(r.relations) && r.relations.length > 0 ? r.relations.join("、") : "";
|
|
5562
|
+
mergedRoles.push(`- ${r.name}(${labelName[r.roleLabel]}):${r.identity}${traits.length > 0 ? `;性格:${traits.join("、")}` : ""}${goals !== "" ? `;目标:${goals}` : ""}${relations !== "" ? `;关系:${relations}` : ""}`);
|
|
5563
|
+
if (card !== void 0 && Array.isArray(card.knowledge) && card.knowledge.length > 0) mergedRoles.push(` 已知信息(该角色知道的:${card.knowledge.join(";")};未列出的信息该角色一律不知道,不得写其知晓或提及)`);
|
|
5564
|
+
}
|
|
5565
|
+
for (const card of bible.characters) {
|
|
5566
|
+
if (seenRole.has(card.name)) continue;
|
|
5567
|
+
seenRole.add(card.name);
|
|
5568
|
+
const roleName = {
|
|
5569
|
+
protagonist: "主角",
|
|
5570
|
+
supporting: "配角",
|
|
5571
|
+
antagonist: "反派",
|
|
5572
|
+
other: "其他"
|
|
5573
|
+
}[card.role];
|
|
5574
|
+
mergedRoles.push(`- ${card.name}(${roleName}):${card.traits.join("、")}${card.goals !== "" ? `;目标:${card.goals}` : ""}${card.relations !== "" ? `;关系:${card.relations}` : ""}`);
|
|
5575
|
+
if (Array.isArray(card.knowledge) && card.knowledge.length > 0) mergedRoles.push(` 已知信息(该角色知道的:${card.knowledge.join(";")};未列出的信息该角色一律不知道,不得写其知晓或提及)`);
|
|
5576
|
+
}
|
|
5577
|
+
if (mergedRoles.length > 0) {
|
|
5578
|
+
book.push("角色卡(角色库与道藏已合并去重):");
|
|
5579
|
+
book.push(...mergedRoles);
|
|
5580
|
+
}
|
|
5581
|
+
if (bible.redLines.length > 0) book.push("写作红线(违反即失败):\n" + bible.redLines.map((r) => `- ${r}`).join("\n"));
|
|
5582
|
+
if (bible.style.length > 0) book.push("风格要求:\n" + bible.style.map((r) => `- ${r}`).join("\n"));
|
|
5583
|
+
}
|
|
5584
|
+
const worldBlock = renderWorld(project.world);
|
|
5585
|
+
if (worldBlock !== "") book.push(worldBlock);
|
|
5586
|
+
book.push("==================== 全书大纲 ====================");
|
|
5587
|
+
const outlineBlock = project.outline.length > 6e3 ? project.outline.slice(0, 6e3) + "\n…(大纲过长已节选,完整内容见总纲页)" : project.outline;
|
|
5588
|
+
book.push(outlineBlock);
|
|
5589
|
+
book.push("==================== 大纲结束 ====================");
|
|
5590
|
+
const assetsBlock = renderAllAssets(project.assets);
|
|
5591
|
+
if (assetsBlock !== "") book.push(assetsBlock);
|
|
5592
|
+
const slotBlock = renderPromptSlots(project);
|
|
5593
|
+
if (slotBlock !== "") book.push(slotBlock);
|
|
5594
|
+
blocks.push({
|
|
5595
|
+
layer: "book",
|
|
5596
|
+
text: book.join("\n")
|
|
5597
|
+
});
|
|
5598
|
+
const chapter = [];
|
|
5599
|
+
const active = project.foreshadows.filter((f) => f.status === "planted" || f.status === "progressing");
|
|
5600
|
+
if (active.length > 0) {
|
|
5601
|
+
chapter.push("==================== 活跃伏笔(近期需推进或回收的线索) ====================");
|
|
5602
|
+
for (const f of active) chapter.push(`- [${f.status === "planted" ? "已埋设" : "推进中"}] ${f.description}${f.targetChapter !== void 0 ? `(预计 ${f.targetChapter} 章回收)` : ""}`);
|
|
5603
|
+
}
|
|
5604
|
+
const lines = (project.plotlines ?? []).filter((l) => l.status === "active" || l.status === "paused");
|
|
5605
|
+
if (lines.length > 0) {
|
|
5606
|
+
const kindName = {
|
|
5607
|
+
main: "主线",
|
|
5608
|
+
branch: "支线",
|
|
5609
|
+
character: "人物线",
|
|
5610
|
+
mystery: "悬念线"
|
|
5611
|
+
};
|
|
5612
|
+
chapter.push("==================== 剧情线(本章应推进至少一条活跃线) ====================");
|
|
5613
|
+
for (const l of lines) chapter.push(`- [${kindName[l.kind]}${l.status === "paused" ? "·暂停中" : ""}] ${l.name}:${l.goal}${l.progress !== "" ? `(当前进度:${l.progress})` : ""}`);
|
|
5614
|
+
}
|
|
5615
|
+
chapter.push(renderChapterWriterSkeleton({
|
|
5616
|
+
targetChars: target,
|
|
5617
|
+
minChars: lo,
|
|
5618
|
+
maxChars: hi,
|
|
5619
|
+
pov: "第三人称有限视角,严格跟随主角所见所知。",
|
|
5620
|
+
endingHookPreference: "章末留一个明确钩子(新信息、新风险或未闭合选择)。",
|
|
5621
|
+
tonePreference: "动作、对话、心理交替推进;重大信息用对话/动作/发现呈现。",
|
|
5622
|
+
antiAiRules: "严格遵循上方「反 AI 规则」与写法资产,避免套话与 AI 腔。"
|
|
5623
|
+
}));
|
|
5624
|
+
const timelineBlock = renderTimelineBlock(project, beforeChapter ?? Number.MAX_SAFE_INTEGER);
|
|
5625
|
+
if (timelineBlock !== "") chapter.push(timelineBlock);
|
|
5626
|
+
if (beforeChapter !== void 0) {
|
|
5627
|
+
const tensionBlock = renderTensionBlock(project, beforeChapter);
|
|
5628
|
+
if (tensionBlock !== "") chapter.push(tensionBlock);
|
|
5629
|
+
}
|
|
5630
|
+
blocks.push({
|
|
5631
|
+
layer: "chapter",
|
|
5632
|
+
text: chapter.join("\n")
|
|
5633
|
+
});
|
|
5634
|
+
return assemblePromptBlocks(blocks);
|
|
5635
|
+
}
|
|
5636
|
+
/** Build the rewrite system prompt (fix review issues / instructions). */
|
|
5637
|
+
function rewriteSystemPrompt(project, targetChars, beforeChapter) {
|
|
5638
|
+
return writeSystemPrompt(project, targetChars, "1. 输出完整的新正文(不要只输出修改片段、标题、章回名、作者的话或任何 Markdown 标记),字数与原章相当(允许 ±20%)。", beforeChapter) + "\n\n额外要求:你正在【修订】一章已写好的正文。保留原文中好的部分,只修改需要修改的地方,输出完整的新正文(不要只输出修改片段),字数与原文相当。";
|
|
5639
|
+
}
|
|
5640
|
+
/**
|
|
5641
|
+
* 正文续写补齐的附加要求(拼在 writeSystemPrompt 之后)。
|
|
5642
|
+
*
|
|
5643
|
+
* 触发场景:生成的正文不足目标字数 90% 时,最多补两轮。
|
|
5644
|
+
*/
|
|
5645
|
+
function bodyContinuationSuffix() {
|
|
5646
|
+
return "\n\n【续写任务】严格承接已有正文往下写,不要重复已有内容、不要重写开头、不要总结前文,直接续写新情节,并在结尾留一个章末钩子。";
|
|
5647
|
+
}
|
|
5648
|
+
//#endregion
|
|
5649
|
+
//#region src/prompts/review.ts
|
|
5650
|
+
/** The review system prompt. */
|
|
5651
|
+
function reviewSystemPrompt(project) {
|
|
5652
|
+
const bible = project.bible;
|
|
5653
|
+
const blocks = [];
|
|
5654
|
+
const fixed = [
|
|
5655
|
+
"你是一位严格的网文审稿编辑。你会收到一章正文以及本书的道藏与红线。",
|
|
5656
|
+
"请从以下维度审查本章:",
|
|
5657
|
+
"1. 人设一致性:角色行为是否符合下方角色卡的设定(性格/目标/知情度/说话方式)。",
|
|
5658
|
+
"2. 设定一致性:金手指规则、战力体系、世界观是否与道藏冲突。",
|
|
5659
|
+
"3. 红线检查:是否触犯下方「本书红线」与「内容合规红线」。",
|
|
5660
|
+
"4. 文笔质量:语病、翻译腔、AI 套话(\"不禁\"\"仿佛\"\"一时间\"等高频词滥用)、流水账。",
|
|
5661
|
+
"5. 节奏与爽点:本章是否有推进、有钩子,是否拖沓灌水。",
|
|
5662
|
+
"6. 逻辑漏洞:前后矛盾、时间线错误、对话失真。",
|
|
5663
|
+
"7. 反 AI 规则:下方「本地 AI 味扫描」里已给出**本轮确定性命中的规则与次数**,直接以它为事实依据判级(禁止类命中即列为问题;鼓励类只作低优先级建议、不阻塞通过)。未列出的规则说明本章无确定性命中,不必逐条复述,也不要凭印象补报。",
|
|
5664
|
+
"8. 呈现方式:整章是否纯内心推理铺陈(无对话/无对抗,推理全靠解说);反派是否纯背景板无行动;重要配角是否无名标签化(瘦高个/灰衣人全程代称)——命中即列为问题。",
|
|
5665
|
+
"9. 内容合规(最高优先级):逐条核对下方「内容合规红线」,任何一条命中(含影射、暗示、详细描写)必须列为 high,并给出改写建议。",
|
|
5666
|
+
"输出必须是合法 JSON 对象,不要输出任何其他文字:",
|
|
5667
|
+
"{\"score\": 0-100的整数, \"riskScore\": 0-100的整数(越高越需人工处理,可结合本地AI味指数), \"tension\": 0-100的整数(本章张力强度:对抗/揭示/抉择/代价;铺垫与日常章应明显偏低), \"verdict\": \"一句话总评\", \"issues\": [{\"severity\": \"high|medium|low\", \"dimension\": \"character|setting|redline|writing|pacing|logic|anti-ai|presentation|compliance\", \"item\": \"问题描述\", \"suggestion\": \"修改建议\", \"ruleName\": \"命中的反AI规则名(见反AI规则清单)\", \"ruleType\": \"forbidden|risk|encourage\", \"category\": \"套话|句式|段落|心理|设定|节奏|对话|其他\", \"excerpt\": \"命中的原文摘录(不超过50字)\", \"reason\": \"判定理由\", \"canAutoRewrite\": true|false}]}",
|
|
5668
|
+
"tension 是本章张力强度评分(不是质量分):对决/揭秘/生死抉择偏高,铺垫章、日常章、过渡章偏低;同一本书内保持同一尺度。",
|
|
5669
|
+
"反 AI 类 issue 尽量给出 ruleName/ruleType/category/excerpt/reason/canAutoRewrite,便于统计与自动改写。",
|
|
5670
|
+
"风格符合度:若下方给出「风格指纹比对」,以它为准判断本章是否偏离绑定写法——偏差显著时按 writing 维度、category 用「风格」上报;比对里说没有显著偏差时,不要报风格问题。",
|
|
5671
|
+
"AI 套话高频模板词示例(集中出现必须整体降密度):仿佛、似乎、极其、完美、深不见底、形成了、莫名、无法形容、难以言喻、精心雕琢、肤光胜雪、眉目如画、歌舞升平、觥筹交错、妙语连珠、不可名状、另一层真相、命运、真相。",
|
|
5672
|
+
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。",
|
|
5673
|
+
"重要:直接输出 JSON 结果本身,不要把思考过程写在输出里。",
|
|
5674
|
+
"==================== 内容合规红线(平台硬性要求,最高优先级) ====================",
|
|
5675
|
+
COMPLIANCE_REDLINES.join("\n"),
|
|
5676
|
+
"以上九条为硬性底线:正文中任何一条命中(含影射、暗示、详细展开)都必须列为 high,并给出改写建议;作者自定义红线不得豁免这九条。"
|
|
5677
|
+
];
|
|
5678
|
+
blocks.push({
|
|
5679
|
+
layer: "fixed",
|
|
5680
|
+
text: fixed.join("\n")
|
|
5681
|
+
});
|
|
5682
|
+
const book = [];
|
|
5683
|
+
const assetsBlock = renderAllAssets(project.assets);
|
|
5684
|
+
if (assetsBlock !== "") book.push("\n" + assetsBlock);
|
|
5685
|
+
if (bible !== void 0) {
|
|
5686
|
+
book.push("\n==================== 道藏 ====================");
|
|
5687
|
+
if (bible.worldRules.length > 0) book.push("世界规则:\n" + bible.worldRules.map((r) => `- ${r}`).join("\n"));
|
|
5688
|
+
if (bible.characters.length > 0) {
|
|
5689
|
+
book.push("角色卡:");
|
|
5690
|
+
for (const card of bible.characters) {
|
|
5691
|
+
book.push(`- ${card.name}(${card.role}):${card.traits.join("、")}`);
|
|
5692
|
+
if (Array.isArray(card.knowledge) && card.knowledge.length > 0) book.push(` 该角色知道:${card.knowledge.join(";")}(未列出的信息该角色不知道)`);
|
|
5693
|
+
}
|
|
5694
|
+
}
|
|
5695
|
+
if (bible.redLines.length > 0) book.push("红线:\n" + bible.redLines.map((r) => `- ${r}`).join("\n"));
|
|
5696
|
+
}
|
|
5697
|
+
blocks.push({
|
|
5698
|
+
layer: "book",
|
|
5699
|
+
text: book.join("\n")
|
|
5700
|
+
});
|
|
5701
|
+
return assemblePromptBlocks(blocks);
|
|
5702
|
+
}
|
|
5703
|
+
/** 验证模式系统提示:修订后逐条核对原意见是否解决,只挑新增 high,不重复挑剔主观项。 */
|
|
5704
|
+
function verifySystemPrompt(project) {
|
|
5705
|
+
return [
|
|
5706
|
+
"你是一位网文审稿验证员。作者已按上一轮审稿意见修订了本章,你需要验证修订效果。",
|
|
5707
|
+
"你的任务(严格按此执行):",
|
|
5708
|
+
"1. 逐条核对「上一轮意见」中的每一条(按编号 1、2、3...)是否已在修订稿中解决。",
|
|
5709
|
+
"2. 只挑修订【新引入】的 high 级问题(设定矛盾/逻辑硬伤/事实错误)——新引入的 medium/low 主观项(文笔/套话/节奏)不要列。",
|
|
5710
|
+
"3. 禁止重复挑剔上一轮已指出且本次已解决的主观项(如\"缓缓/微微\"等套话、错别字)——即使换个说法再提也不行。",
|
|
5711
|
+
"4. 严禁为了显得专业而新增\"换一批毛病\"式的意见。",
|
|
5712
|
+
"输出必须是合法 JSON 对象,包含以下字段:",
|
|
5713
|
+
"- resolvedIds:已解决的上一轮意见编号数组(如 [1, 3, 5])。",
|
|
5714
|
+
"- unresolvedIds:未解决或部分解决的上一轮意见编号数组(如 [2, 4])。",
|
|
5715
|
+
"- issues:未解决的原意见 + 新引入的 high 级问题列表(格式同审稿:severity/item/suggestion)。未解决的原意见 item 需注明\"未解决(编号N):原意见摘要\"。",
|
|
5716
|
+
"- score:按修订稿整体质量给 50-90 分(解决全部 high 且无新增 high 时给 70 以上)。",
|
|
5717
|
+
"- verdict:一句话结论。",
|
|
5718
|
+
"完整格式:{\"resolvedIds\": [1,3], \"unresolvedIds\": [2], \"score\": 75, \"verdict\": \"一句话\", \"issues\": [{\"severity\": \"high\", \"dimension\": \"character|setting|redline|writing|pacing|logic|anti-ai|presentation|compliance\", \"item\": \"未解决(2):xxx\", \"suggestion\": \"xxx\"}]}",
|
|
5719
|
+
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。",
|
|
5720
|
+
"重要:直接输出 JSON 结果本身,不要把思考过程写在输出里。",
|
|
5721
|
+
renderBibleForVerify(project)
|
|
5722
|
+
].join("\n");
|
|
5723
|
+
}
|
|
5724
|
+
/**
|
|
5725
|
+
* 验证模式下注入道藏。
|
|
5726
|
+
*
|
|
5727
|
+
* 原实现是 `JSON.stringify(project.bible).slice(0, 3000)`——有两个问题:
|
|
5728
|
+
* 1. **腰斩的 JSON 不是 JSON**:`slice` 从中间切断,审稿员会看到语法残缺的
|
|
5729
|
+
* 对象(字符串没闭合、数组没结尾),既浪费 token 又可能误读设定。
|
|
5730
|
+
* 2. **静默截断设定**:世界规则/角色卡排在后面的字段最先被切掉,而那恰恰是
|
|
5731
|
+
* 核对设定冲突最需要的部分。
|
|
5732
|
+
*
|
|
5733
|
+
* 改为结构化渲染 + 按「世界规则/角色/红线」分配独立预算。
|
|
5734
|
+
*/
|
|
5735
|
+
function renderBibleForVerify(project) {
|
|
5736
|
+
const bible = project.bible;
|
|
5737
|
+
if (bible === void 0) return "本书道藏(核对设定冲突用):\n(无)";
|
|
5738
|
+
const parts = [];
|
|
5739
|
+
if (bible.worldRules.length > 0) parts.push(`世界规则:\n${bible.worldRules.map((r) => `- ${r}`).join("\n")}`);
|
|
5740
|
+
if (bible.characters.length > 0) parts.push(`角色卡:\n${bible.characters.map((c) => {
|
|
5741
|
+
const goals = c.goals !== "" ? `;目标:${c.goals}` : "";
|
|
5742
|
+
const rel = c.relations !== "" ? `;关系:${c.relations}` : "";
|
|
5743
|
+
return `- ${c.name}:${c.traits.join("、")}${goals}${rel}`;
|
|
5744
|
+
}).join("\n")}`);
|
|
5745
|
+
if (bible.redLines.length > 0) parts.push(`红线:\n${bible.redLines.map((r) => `- ${r}`).join("\n")}`);
|
|
5746
|
+
if (parts.length === 0) return "本书道藏(核对设定冲突用):\n(无)";
|
|
5747
|
+
const block = parts.join("\n");
|
|
5748
|
+
const limit = 6e3;
|
|
5749
|
+
return `本书道藏(核对设定冲突用):\n${block.length > limit ? block.slice(0, limit) + `\n…(道藏过长已节选,但角色名/世界规则已尽量保留;如需完整设定请查总纲页)` : block}`;
|
|
5750
|
+
}
|
|
5751
|
+
/** Build the author-review system prompt (narrative structure, not prose). */
|
|
5752
|
+
function authorReviewSystemPrompt() {
|
|
5753
|
+
return [
|
|
5754
|
+
"你是一位网文作者复盘助手。你会收到:本章正文、上一章结尾(钩子)、上一章作者复盘(如有)、活跃剧情线与编年录近期事实。",
|
|
5755
|
+
"请从叙事结构层面复盘本章(不评文笔,那是审稿的事):",
|
|
5756
|
+
"1. hookHonored:上一章结尾的钩子/悬念是否在本章兑现或推进(true/false)。",
|
|
5757
|
+
"2. hookNote:钩子兑现情况一句话;未兑现时说明并给出\"建议在第几章补\"的建议。",
|
|
5758
|
+
"3. endingHook:本章结尾钩子强度,0-10 的整数(低于 6 说明结尾平淡,读者可能不想看下一章)。",
|
|
5759
|
+
"4. plotlineProgress:本章推进了哪条剧情线(主线/支线名),或\"无实质推进\"(连续无推进要提醒)。",
|
|
5760
|
+
"5. advancedLines:本章实际推进的剧情线名称数组——从「活跃剧情线」清单中选出推进了的线(名称必须与清单中的线名一字不差;没推进任何线则输出空数组)。",
|
|
5761
|
+
"6. continuity:与上一章结尾的衔接检查(人物位置/时间/伤势/资源/对话状态),发现问题要指出。",
|
|
5762
|
+
"7. trend:结合上一章复盘看近期节奏趋势(是否连续拖沓、爽点密度是否下降、是否需要调整)。",
|
|
5763
|
+
"8. stateChanges:本章发生的关键状态变化数组(人物状态/世界局面/关系/资源,每条约 20 字,最多 6 条),供整本与事实库回灌。",
|
|
5764
|
+
"9. newConflicts:本章新引入或明显升级的冲突数组(最多 4 条)。",
|
|
5765
|
+
"10. clues:本章埋下或推进的新线索/伏笔数组(最多 4 条)。",
|
|
5766
|
+
"11. absentRisks:本章缺席但按卷级职责应出场/该推进关系的重要角色,及其缺席带来的风险(最多 4 条)。",
|
|
5767
|
+
"输出必须是合法 JSON 对象,不要输出任何其他文字:",
|
|
5768
|
+
"{\"hookHonored\": true或false, \"hookNote\": \"一句话\", \"endingHook\": 0-10整数, \"plotlineProgress\": \"一句话\", \"advancedLines\": [\"线名\"], \"continuity\": \"一句话\", \"trend\": \"一句话\", \"stateChanges\": [\"状态变化\"], \"newConflicts\": [\"新冲突\"], \"clues\": [\"新线索\"], \"absentRisks\": [\"本章缺席但值得注意的角色及风险(卷级职责/缺席风险,最多4条)\"]}",
|
|
5769
|
+
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。",
|
|
5770
|
+
"重要:直接输出 JSON 结果本身,不要把思考过程写在输出里。"
|
|
5771
|
+
].join("\n");
|
|
5772
|
+
}
|
|
5773
|
+
//#endregion
|
|
5774
|
+
//#region src/prompts/polish.ts
|
|
5775
|
+
/** The de-AI-ify polish system prompt (with project writing assets injected). */
|
|
5776
|
+
function polishSystemPrompt(project) {
|
|
5777
|
+
const assetsBlock = renderAllAssets(project.assets);
|
|
5778
|
+
const bible = project.bible;
|
|
5779
|
+
const blocks = [];
|
|
5780
|
+
const fixed = [
|
|
5781
|
+
"你是一位中文网文润色师。你会收到一章正文,请做「去 AI 味」润色:",
|
|
5782
|
+
"1. 删除/替换 AI 高频套话与模式词:如\"不禁\"\"仿佛\"\"一时间\"\"不由得\"\"顿时\"\"然而\"\"缓缓\"\"轻轻\"\"微微\"\"默默\"\"似乎\"\"终于\"等滥用。",
|
|
5783
|
+
"2. 把书面翻译腔改成口语化的中文网文语感。",
|
|
5784
|
+
"3. 拆分过长的排比句与堆砌的修饰语。",
|
|
5785
|
+
"4. 保留全部情节、人物、对话内容、已确立事实不变,只改表达。",
|
|
5786
|
+
"5. 输出完整的新正文,不要输出任何说明文字或 Markdown 标记。",
|
|
5787
|
+
"6. 必须遵守下方「反 AI 规则」与「写法资产」的表达边界;写法资产要求保留的风格特征(句式、台词、节奏)不得在润色中丢失。",
|
|
5788
|
+
"7. 必须遵守下方「内容合规红线」与「本书红线」(如有),任何一条命中(含影射、暗示)都必须避免。",
|
|
5789
|
+
"内容合规红线(平台硬性要求,最高优先级):",
|
|
5790
|
+
COMPLIANCE_REDLINES.join("\n")
|
|
5791
|
+
];
|
|
5792
|
+
blocks.push({
|
|
5793
|
+
layer: "fixed",
|
|
5794
|
+
text: fixed.join("\n")
|
|
5795
|
+
});
|
|
5796
|
+
const book = [];
|
|
5797
|
+
if (bible !== void 0 && bible.redLines.length > 0) book.push("本书红线:\n" + bible.redLines.map((r) => "- " + r).join("\n"));
|
|
5798
|
+
if (assetsBlock !== "") book.push(assetsBlock);
|
|
5799
|
+
blocks.push({
|
|
5800
|
+
layer: "book",
|
|
5801
|
+
text: book.join("\n")
|
|
5802
|
+
});
|
|
5803
|
+
return assemblePromptBlocks(blocks);
|
|
5804
|
+
}
|
|
5805
|
+
/**
|
|
5806
|
+
* 局部修订(只改一个自然段)的系统提示。
|
|
5807
|
+
*
|
|
5808
|
+
* 与整章修订的区别:不套用字数区间、不注入全书资料(大纲/资产/时间线),
|
|
5809
|
+
* 只带合规红线、本书红线、反 AI 套话口径与角色卡——局部改写要的是「表达
|
|
5810
|
+
* 卫生」,不是重新生成。同样按分层组装(约束口径排最前)。
|
|
5811
|
+
*/
|
|
5812
|
+
function localRevisionSystemPrompt(project) {
|
|
5813
|
+
const bible = project.bible;
|
|
5814
|
+
const blocks = [];
|
|
5815
|
+
blocks.push({
|
|
5816
|
+
layer: "fixed",
|
|
5817
|
+
text: [
|
|
5818
|
+
"你是一位中文网文润色师。你会收到一章中的一个段落,请按修改要求重写该段。",
|
|
5819
|
+
"硬性约束:",
|
|
5820
|
+
"1. 只改表达,不改情节走向、人物设定、已确立事实、对话核心内容。",
|
|
5821
|
+
"2. 必须遵守下方「内容合规红线」,任何一条命中(含影射、暗示)都必须避免。",
|
|
5822
|
+
"3. 必须遵守下方「本书红线」(如有)。",
|
|
5823
|
+
"4. 避免 AI 套话:不禁、仿佛、一时间、不由得、顿时、然而、缓缓、轻轻、微微、默默、似乎、终于等滥用。",
|
|
5824
|
+
"5. 保留角色口吻与性格,角色行为需符合下方角色卡(如有)。",
|
|
5825
|
+
"6. 只输出修改后的【这一个段落】的完整新文本,不要输出任何说明、标题或 Markdown 标记。",
|
|
5826
|
+
"内容合规红线(平台硬性要求,最高优先级):",
|
|
5827
|
+
COMPLIANCE_REDLINES.join("\n")
|
|
5828
|
+
].join("\n")
|
|
5829
|
+
});
|
|
5830
|
+
const book = [];
|
|
5831
|
+
if (bible !== void 0) {
|
|
5832
|
+
if (bible.redLines.length > 0) book.push("本书红线:\n" + bible.redLines.map((r) => "- " + r).join("\n"));
|
|
5833
|
+
if (bible.characters.length > 0) {
|
|
5834
|
+
book.push("相关角色卡:");
|
|
5835
|
+
for (const card of bible.characters) book.push("- " + card.name + "(" + card.role + "):" + card.traits.join("、"));
|
|
5836
|
+
}
|
|
5837
|
+
}
|
|
5838
|
+
blocks.push({
|
|
5839
|
+
layer: "book",
|
|
5840
|
+
text: book.join("\n")
|
|
5841
|
+
});
|
|
5842
|
+
return assemblePromptBlocks(blocks);
|
|
5843
|
+
}
|
|
5844
|
+
//#endregion
|
|
5845
|
+
//#region src/prompts/characters.ts
|
|
5846
|
+
/** AI 从全书提炼角色库:大纲 + 道藏 + 编年录 + 章节摘要 → 结构化角色清单。 */
|
|
5847
|
+
function extractRolesSystemPrompt() {
|
|
5848
|
+
return [
|
|
5849
|
+
"你是一位网文角色库管理员。请根据本书的大纲、设定、编年录与章节摘要,提炼完整的角色库。",
|
|
5850
|
+
"覆盖原则:所有在编年录/章节中实际出场或有名有姓的角色都应收录;无名的功能性人物(如\"矮胖姑娘\")用其身份简称收录并标注;反复出现且有剧情作用的身份型角色(站长、律师、警察、法官、店主等)必须收录。",
|
|
5851
|
+
"数量控制:最多输出 20 个角色;覆盖优先——主角、主要反派、重要配角(女主/关键配角)必须全收,所有有名有姓者必收;只有真正一次性路人(无名字、无剧情作用)才可省略。",
|
|
5852
|
+
"重要:正常一部完整故事应提炼 8-20 个角色;若少于 6 个通常说明漏提炼,必须重新核对正文摘录。",
|
|
5853
|
+
"重要:正文中若有明确的主角与主要反派,必须分别以 protagonist / antagonist 收录,禁止遗漏;反派确实未出场时才可省略。",
|
|
5854
|
+
"输出优先级:主角(protagonist)与主要反派(antagonist)必须优先输出并完整刻画,其次女主/重要配角;判断不出名字时用正文中的身份称呼。",
|
|
5855
|
+
"每个角色输出:",
|
|
5856
|
+
"1. name:角色名(或身份简称)。主角/有名配角必须用正文中实际出现的人名(如「沈放」),禁止用「主角(38岁超市理货员)」这类把身份塞进名字的占位名;正文确实没点名时才可用身份简称(如「富商」「灰衣老人」)。",
|
|
5857
|
+
"2. roleLabel:定位——protagonist=主角;female_lead=女主(唯一知己/感情线核心,无后宫前提下只此一位);female_support=重要女配;support=普通配角;antagonist=反派;extra=路人/背景。",
|
|
5858
|
+
"3. identity:身份一句话(宗门/势力/血脉/职业)。",
|
|
5859
|
+
"4. traits:3-6 个性格标签。",
|
|
5860
|
+
"5. goals:目标与动机一句话。",
|
|
5861
|
+
"6. relations:关系网数组,格式[\"角色名(关系)\", ...]。",
|
|
5862
|
+
"7. arc:成长线数组,格式[\"阶段:说明\", ...](如\"出场:祭品身份\"/\"转折:祭祀被中断脱身\")。",
|
|
5863
|
+
"8. knowledge:该角色已经知道的关键信息(3-8 条),不知道的信息不要写进去。",
|
|
5864
|
+
"精简要求:identity 控制在 30 字内;traits 3-6 个短标签;goals 60 字内;relations 2-5 条;arc 2-4 条;knowledge 每条 40 字内。整体输出量要紧凑,避免冗长。",
|
|
5865
|
+
"重要:用户消息里列出的「已收录角色」绝不要再次输出——这些角色已经在角色库里,跳过它们,只提炼未收录的。",
|
|
5866
|
+
"输出必须是合法 JSON 数组,不要输出其他文字:[{\"name\":\"...\", \"roleLabel\":\"...\", \"identity\":\"...\", \"traits\":[...], \"goals\":\"...\", \"relations\":[...], \"arc\":[...], \"knowledge\":[...]}]",
|
|
5867
|
+
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。",
|
|
5868
|
+
"重要:直接输出 JSON 结果本身,不要把思考过程或推理内容写在输出里。"
|
|
5869
|
+
].join("\n");
|
|
5870
|
+
}
|
|
5871
|
+
/**
|
|
5872
|
+
* 角色补漏后缀(拼在 extractRolesSystemPrompt 之后)。
|
|
5873
|
+
*
|
|
5874
|
+
* 第一轮偶发漏掉「身份型/功能性角色」(站长/律师/警察等反复出现者),
|
|
5875
|
+
* 用这一轮专项补捞;没有遗漏就输出 []。
|
|
5876
|
+
*/
|
|
5877
|
+
function roleCatchUpSuffix(names) {
|
|
5878
|
+
return "\n上一次已提炼角色:" + names + "。\n现在只输出「遗漏的角色」JSON 数组:检查正文摘录中反复出现、有固定身份称呼(如站长、律师、警察、法官、店主、老师)且对剧情有作用的角色;一次性路人不要输出。没有遗漏就输出 []。字段与上面相同。";
|
|
5879
|
+
}
|
|
5880
|
+
/** 角色当前状态聚合:角色名单 + 已确立事实库 → 每角色一句话状态。 */
|
|
5881
|
+
function roleStatusSystemPrompt() {
|
|
5882
|
+
return [
|
|
5883
|
+
"你是一位网文角色档案管理员。请根据「角色名单」与「已确立事实库」,为每个角色输出「当前状态」一句话(境界/修为/伤势/资源/心境)。",
|
|
5884
|
+
"输出必须是合法 JSON 数组,格式:[{\"name\": \"角色名\", \"status\": \"当前状态一句话\"}]",
|
|
5885
|
+
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。"
|
|
5886
|
+
].join("\n");
|
|
5887
|
+
}
|
|
5888
|
+
//#endregion
|
|
5889
|
+
//#region src/prompts/plotlines.ts
|
|
5890
|
+
/** 建议剧情线:基于大纲/卷计划/已写章节/编年录,提炼候选线。 */
|
|
5891
|
+
function suggestPlotlinesSystemPrompt() {
|
|
5892
|
+
return [
|
|
5893
|
+
"你是一位网文剧情架构师。根据本书的大纲、卷计划、已写章节标题与编年录,为作者提炼建议的剧情线(主线/支线/人物线/悬念线)。",
|
|
5894
|
+
"每条线要:名称简洁有力;目标写清楚这条线最终要完成什么;progress 写当前推进到哪(没有就空字符串)。",
|
|
5895
|
+
"建议 4-8 条,覆盖:1 条主线、1-2 条人物线、1-2 条悬念线、1-3 条支线。避免与大纲明显重复的废话线。",
|
|
5896
|
+
"输出必须是合法 JSON 数组,格式:[{\"name\": \"线名\", \"kind\": \"main|branch|character|mystery\", \"goal\": \"目标\", \"progress\": \"\"}]",
|
|
5897
|
+
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。",
|
|
5898
|
+
"重要:直接输出 JSON 结果本身,不要把思考过程或推理内容写在输出里。"
|
|
5899
|
+
].join("\n");
|
|
5900
|
+
}
|
|
5901
|
+
/**
|
|
5902
|
+
* 剧情线进度刷新(批量)。
|
|
5903
|
+
*
|
|
5904
|
+
* @param count - 本次要判断的线数。单条时问单线进度,多条时要求逐条判断、
|
|
5905
|
+
* name 与输入逐字相同(用于对齐回写,见 plotline-align.ts)。
|
|
5906
|
+
*/
|
|
5907
|
+
function refreshPlotlinesSystemPrompt(count) {
|
|
5908
|
+
return [
|
|
5909
|
+
count === 1 ? "你是一位网文剧情线管理员。请根据「剧情线信息」与「本书已写章节摘要/编年录」,判断这条线目前推进到了哪一步。" : `你是一位网文剧情线管理员。下面给了 ${count} 条剧情线,请**逐条**判断各自推进到了哪一步。`,
|
|
5910
|
+
"每条线输出一句话(30-60 字):该线当前的状态、最近一次推进发生在第几章、下一步可能的方向。还没开始推进的线,明确说\"尚未推进\"。",
|
|
5911
|
+
count === 1 ? "输出必须是合法 JSON 对象:{\"progress\": \"一句话\"}" : `输出必须是合法 JSON 对象:{"items": [{"name": "线名(与输入完全一致)", "progress": "一句话"}]},items 的条数必须等于 ${count},name 必须与输入里的线名逐字相同(用于对齐)`,
|
|
5912
|
+
"重要:不要输出任何其他文字。"
|
|
5913
|
+
].join("\n");
|
|
5914
|
+
}
|
|
5915
|
+
/** 剧情健康检查:基于已写章节数/各线状态/编年录,判断是否需要新线及添加时机。 */
|
|
5916
|
+
function plotlineHealthSystemPrompt() {
|
|
5917
|
+
return [
|
|
5918
|
+
"你是一位网文剧情架构师。请对本书的「剧情线体系」做健康检查,判断当前是否需要新增剧情线、应在多少章后添加。",
|
|
5919
|
+
"评估维度:各线最近推进到第几章(已写章节与关联章节的差值越大越危险)、各线状态、已写章节总数、卷计划当前进度、编年录近期事实。",
|
|
5920
|
+
"输出规则:",
|
|
5921
|
+
"1. verdict:一句话结论——\"需要新增线\" / \"暂不需要\" / \"再写 N 章后需要\"(N 给出具体章数)。",
|
|
5922
|
+
"2. timing:说明建议添加的时机(如:第 25 章前引入新支线,因为主线预计第 22 章告一段落)。",
|
|
5923
|
+
"3. reasons:3-5 条依据(引用具体数据:哪条线多少章没推进、已写章节数、卷进度等)。",
|
|
5924
|
+
"4. lines:对每条线给健康度——ok(近期推进过)/ warning(超过 5 章未推进)/ stale(超过 10 章未推进或悬置过久)。",
|
|
5925
|
+
"输出必须是合法 JSON 对象:{\"verdict\": \"...\", \"timing\": \"...\", \"reasons\": [\"...\"], \"lines\": [{\"name\": \"线名\", \"health\": \"ok|warning|stale\", \"note\": \"一句说明\"}]}",
|
|
5926
|
+
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。",
|
|
5927
|
+
"重要:直接输出 JSON 结果本身,不要把思考过程或推理内容写在输出里。"
|
|
5928
|
+
].join("\n");
|
|
5929
|
+
}
|
|
5930
|
+
/** 剧情方案:基于健康检查结果设计下一阶段方向与建议新线。 */
|
|
5931
|
+
function plotlinePlanSystemPrompt() {
|
|
5932
|
+
return [
|
|
5933
|
+
"你是一位网文剧情架构师。请为本书设计「下一阶段的剧情方案」:给出未来 5-10 章的剧情方向,并建议 2-3 条值得新增的剧情线。",
|
|
5934
|
+
"要求:方向必须结合本书大纲/卷计划/现有线/编年录;新线要能落地(和当前主角处境、已有伏笔、下一阶段舞台相关),不得重复已有线。",
|
|
5935
|
+
"输出必须是合法 JSON 对象:{\"direction\": \"下一阶段方向 60-120 字\", \"suggestions\": [{\"name\": \"线名\", \"kind\": \"main|branch|character|mystery\", \"goal\": \"目标\", \"progress\": \"初始进度(可空)\"}]}",
|
|
5936
|
+
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。",
|
|
5937
|
+
"重要:直接输出 JSON 结果本身,不要把思考过程或推理内容写在输出里。"
|
|
5938
|
+
].join("\n");
|
|
5939
|
+
}
|
|
5940
|
+
//#endregion
|
|
5941
|
+
//#region src/prompts/foreshadow.ts
|
|
5942
|
+
/** System prompt for foreshadow suggestions. */
|
|
5943
|
+
function foreshadowSystemPrompt() {
|
|
5944
|
+
return [
|
|
5945
|
+
"你是一位网文伏笔设计师。你会收到大纲和已写的章节信息,请为小说建议 3-8 条值得埋设的伏笔。",
|
|
5946
|
+
"要求:",
|
|
5947
|
+
"1. 伏笔必须有明确的回收价值(推动主线、人物弧光、世界观揭秘)。",
|
|
5948
|
+
"2. 描述要具体,指出埋设章节与预计回收章节(可空缺)。",
|
|
5949
|
+
"3. 优先从大纲的暗线(如记忆代价、残片收集、身世谜团)中提炼。",
|
|
5950
|
+
"输出必须是合法 JSON 数组:",
|
|
5951
|
+
"[{\"description\": \"伏笔描述\", \"plantedChapter\": 章节号或null, \"targetChapter\": 章节号或null}]",
|
|
5952
|
+
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。"
|
|
5953
|
+
].join("\n");
|
|
5954
|
+
}
|
|
5955
|
+
//#endregion
|
|
5956
|
+
//#region src/prompts/outline.ts
|
|
5957
|
+
/** 创意灵感:一句话/题材 → 多方向开书灵感。 */
|
|
5958
|
+
function ideaInspirationSystemPrompt() {
|
|
5959
|
+
return ["你是中文网文开书灵感策划。根据用户一句话/题材方向,给出多个可开书的差异化创意。", "严禁输出具体人名/作品名;用身份/通用称谓。每个创意都要有明确钩子、题材、视角、长期兑现。"].join("\n");
|
|
5960
|
+
}
|
|
5961
|
+
/**
|
|
5962
|
+
* 大纲方案:作者的一句「想法」→ 2-3 个方向差异明显的完整大纲。
|
|
5963
|
+
*
|
|
5964
|
+
* @param n - 本次要输出的方案数(1-3)。
|
|
5965
|
+
*/
|
|
5966
|
+
function outlineCandidatesSystemPrompt(n) {
|
|
5967
|
+
return [
|
|
5968
|
+
"你是一位资深网文策划。作者只给了一句「想法」,你需要把它扩展成 2-3 个【方向差异明显】的完整小说大纲方案,供作者挑选。",
|
|
5969
|
+
"每个方案必须满足:",
|
|
5970
|
+
"1. bookName:书名(6 字以内,抓眼球、点题)。",
|
|
5971
|
+
"2. genre:题材(如 仙侠修真 / 都市异能 / 玄幻 / 悬疑)。",
|
|
5972
|
+
"3. sellingPoint:核心卖点一句话(金手指/爽点/差异化,40 字内)。",
|
|
5973
|
+
"4. outline:完整大纲文本(至少 800 字,可直接作为开书大纲),结构包含:书名与题材、金手指/核心设定、主角人设与动机、主线剧情走向(至少 5 个阶段)、关键配角与势力、卖点与爽点设计、预计分卷(3-5 卷)。",
|
|
5974
|
+
"方向差异要求:",
|
|
5975
|
+
"- 方案之间的金手指/剧情走向必须明显不同(如:苟道发育流 vs 随身老爷爷流 vs 群像争霸流),不能只是换书名。",
|
|
5976
|
+
"- 忠实于作者想法的核心要素,但允许在不同方向上进行合理演绎。",
|
|
5977
|
+
"- 不输出任何与已列「需避开的方向」雷同的方案。",
|
|
5978
|
+
"输出必须是合法 JSON 数组,只输出数组本身:",
|
|
5979
|
+
"[{\"id\": \"唯一id\", \"bookName\": \"...\", \"genre\": \"...\", \"sellingPoint\": \"...\", \"outline\": \"...\"}]",
|
|
5980
|
+
`本次只输出 ${n} 个方案。`,
|
|
5981
|
+
"重要:所有字符串值内部不得包含换行符(大纲内部分段请用「。\n」或「;」自然断句),JSON 必须在一段内完整结束。",
|
|
5982
|
+
"重要:直接输出 JSON 结果本身,不要把思考过程或推理内容写在输出里。"
|
|
5983
|
+
].join("\n");
|
|
5984
|
+
}
|
|
5985
|
+
/** 反推大纲 · 阶段一:分批为每章输出一行「事件摘要」。 */
|
|
5986
|
+
function reverseOutlineBatchSystemPrompt() {
|
|
5987
|
+
return "你是一位网文编辑。下面是一本书若干章正文的节选。请为每一章输出一行「事件摘要」,格式严格为:第N章《标题》:关键事件+主角状态变化+新增伏笔或线索。每章恰好一行,不要空行,不要评价,不要输出其他内容。";
|
|
5988
|
+
}
|
|
5989
|
+
/** 反推大纲 · 阶段二:各章事件摘要 → 全书总纲(Markdown)。 */
|
|
5990
|
+
function reverseOutlineSystemPrompt() {
|
|
5991
|
+
return [
|
|
5992
|
+
"你是一位经验丰富的小说主编。根据下面全书各章事件摘要,反推出这本书的总纲(大纲),可直接作为后续写作依据。要求:",
|
|
5993
|
+
"1. 第一行写《书名》(从摘要中的书名或内容推断,若无则用《未命名》)。",
|
|
5994
|
+
"2. 按故事弧线划分卷/部分:每卷给出卷名与主旨(标注覆盖章节范围)。",
|
|
5995
|
+
"3. 每一章列出:章节号 + 标题 + 一句话核心情节(若原章无标题可自拟)。",
|
|
5996
|
+
"4. 最后给出:全书主线、主要人物弧线、已埋设待回收的伏笔清单。",
|
|
5997
|
+
"5. 输出为纯文本 Markdown 结构(# 一级标题、## 二级标题、- 列表),不要多余寒暄。"
|
|
5998
|
+
].join("\n");
|
|
5999
|
+
}
|
|
6000
|
+
/** 反推大纲 · 改编线:从拆章节选反推总纲,作为改编与续写的骨架。 */
|
|
6001
|
+
function reverseOutlineFromAdaptationSystemPrompt() {
|
|
6002
|
+
return [
|
|
6003
|
+
"你是一位经验丰富的小说主编。根据下面若干章节的正文节选,反推出这本书的总纲(可作为后续改编与章节续写的骨架)。",
|
|
6004
|
+
"要求:",
|
|
6005
|
+
"1. 第一行写《书名》(可从正文推断,否则《未命名》)。",
|
|
6006
|
+
"2. 按故事弧线划分卷/部分:每卷给卷名与主旨(标注覆盖章节范围)。",
|
|
6007
|
+
"3. 对每章列出:章节号 + 标题 + 一句话核心情节。",
|
|
6008
|
+
"4. 最后给出:全书主线、主要人物弧线、已埋设待回收的伏笔清单。",
|
|
6009
|
+
"5. 输出纯文本 Markdown 结构(# 一级标题、## 二级标题、- 列表),不要寒暄,不要其他输出。"
|
|
6010
|
+
].join("\n");
|
|
6011
|
+
}
|
|
6012
|
+
//#endregion
|
|
6013
|
+
//#region src/prompts/digest.ts
|
|
6014
|
+
/** 拆书阶段一:单章正文 → 结构化笔记(供后续章节级分析复用)。 */
|
|
6015
|
+
function breakdownNoteSystemPrompt() {
|
|
6016
|
+
return [
|
|
6017
|
+
"你是中文网文拆书助手。把单章正文整理成结构化笔记,供后续章节级分析复用。",
|
|
6018
|
+
"只输出 JSON 对象:",
|
|
6019
|
+
"{\"summary\": \"1-2句\", \"plotPoints\": [\"...\"], \"characters\": [\"...\"], \"worldbuilding\": [\"...\"], \"styleTechniques\": [\"...\"], \"marketHighlights\": [\"...\"], \"weaknessSignals\": [\"...\"]}",
|
|
6020
|
+
"硬规则:只提取正文明确出现的信息;每数组最多 4 项;不要补写原文外的动机/意图;evidence 不在此阶段输出。",
|
|
6021
|
+
"重要:直接输出 JSON,不要输出其他文字;字符串内不含换行。"
|
|
6022
|
+
].join("\n");
|
|
6023
|
+
}
|
|
6024
|
+
/**
|
|
6025
|
+
* 拆书阶段二:分节分析配置。
|
|
6026
|
+
*
|
|
6027
|
+
* @param preset - quick(总览/剧情/人物/文风)| standard(+ 商业化卖点)。
|
|
6028
|
+
*/
|
|
6029
|
+
function breakdownSections(preset) {
|
|
6030
|
+
const sections = [
|
|
6031
|
+
{
|
|
6032
|
+
key: "overview",
|
|
6033
|
+
title: "拆书总览",
|
|
6034
|
+
focus: "一句话定位、题材标签、整体优势与短板",
|
|
6035
|
+
system: [
|
|
6036
|
+
"你是资深中文网文拆书分析师,负责《拆书总览》小节。",
|
|
6037
|
+
"基于给定章节笔记做低风险综合判断,输出 JSON:{\"markdown\": \"可直接展示的分析稿(简体中文,先给结论再说明体现在哪、为何成立)\", \"structured\": {\"oneLinePositioning\": \"一句话定位\", \"genreTags\": [\"题材标签\"], \"sellingPointTags\": [\"卖点标签\"], \"strengths\": [\"整体优势\"], \"weaknesses\": [\"整体短板\"]}}",
|
|
6038
|
+
"硬规则:只基于笔记归纳;推断用「更偏向/可能」等谨慎措辞;证据不足写「材料不足」;不虚构原文细节。",
|
|
6039
|
+
"重要:直接输出 JSON,字符串内不含换行。"
|
|
6040
|
+
].join("\n")
|
|
6041
|
+
},
|
|
6042
|
+
{
|
|
6043
|
+
key: "plot",
|
|
6044
|
+
title: "剧情结构",
|
|
6045
|
+
focus: "主线梗概、阶段推进、冲突升级、节奏风险",
|
|
6046
|
+
system: [
|
|
6047
|
+
"你是资深中文网文拆书分析师,负责《剧情结构》小节。",
|
|
6048
|
+
"基于给定章节笔记分析,输出 JSON:{\"markdown\": \"分析稿(简体中文,先结论后依据)\", \"structured\": {\"mainlineSummary\": \"主线梗概\", \"phaseProgressions\": [\"阶段推进\"], \"escalationDesigns\": [\"冲突升级\"], \"paceRisks\": [\"节奏风险\"], \"reusablePatterns\": [\"可复用套路\"]}}",
|
|
6049
|
+
"硬规则:只基于笔记归纳;推断谨慎措辞;不虚构。",
|
|
6050
|
+
"重要:直接输出 JSON,字符串内不含换行。"
|
|
6051
|
+
].join("\n")
|
|
6052
|
+
},
|
|
6053
|
+
{
|
|
6054
|
+
key: "character",
|
|
6055
|
+
title: "人物系统",
|
|
6056
|
+
focus: "主角定位、配角功能、关系网络、成长弧线、辨识度风险",
|
|
6057
|
+
system: [
|
|
6058
|
+
"你是资深中文网文拆书分析师,负责《人物系统》小节。",
|
|
6059
|
+
"基于给定章节笔记分析,输出 JSON:{\"markdown\": \"分析稿(简体中文,先结论后依据)\", \"structured\": {\"protagonistPositioning\": \"主角定位\", \"supportingFunctions\": [\"配角功能\"], \"relationshipNetwork\": [\"关系网络\"], \"growthArcs\": [\"成长弧线\"], \"clarityRisks\": [\"辨识度风险\"]}}",
|
|
6060
|
+
"硬规则:只基于笔记归纳;推断谨慎措辞;不虚构。",
|
|
6061
|
+
"重要:直接输出 JSON,字符串内不含换行。"
|
|
6062
|
+
].join("\n")
|
|
6063
|
+
},
|
|
6064
|
+
{
|
|
6065
|
+
key: "style",
|
|
6066
|
+
title: "文风与技法",
|
|
6067
|
+
focus: "叙事视角、语言风格、描写方式、节奏控制、钩子设计、可复用写法",
|
|
6068
|
+
system: [
|
|
6069
|
+
"你是资深中文网文拆书分析师,负责《文风与技法》小节。",
|
|
6070
|
+
"基于给定章节笔记分析,输出 JSON:{\"markdown\": \"分析稿(简体中文,先结论后依据)\", \"structured\": {\"narrativePov\": \"叙事视角\", \"languageStyle\": \"语言风格\", \"dialoguePatterns\": [\"对话特征\"], \"rhythmControl\": [\"节奏控制\"], \"hookDesigns\": [\"钩子设计\"], \"reusableTechniques\": [\"可复用写法\"]}}",
|
|
6071
|
+
"硬规则:只基于笔记归纳;推断谨慎措辞;不虚构。",
|
|
6072
|
+
"重要:直接输出 JSON,字符串内不含换行。"
|
|
6073
|
+
].join("\n")
|
|
6074
|
+
}
|
|
6075
|
+
];
|
|
6076
|
+
if (preset === "standard") sections.push({
|
|
6077
|
+
key: "market",
|
|
6078
|
+
title: "商业化卖点",
|
|
6079
|
+
focus: "读者爽点、点击驱动、人物/题材卖点、商业化风险",
|
|
6080
|
+
system: [
|
|
6081
|
+
"你是资深中文网文拆书分析师,负责《商业化卖点》小节。",
|
|
6082
|
+
"基于给定章节笔记分析,输出 JSON:{\"markdown\": \"分析稿(简体中文,先结论后依据)\", \"structured\": {\"hookPoints\": [\"读者爽点\"], \"clickDrivers\": [\"点击驱动\"], \"characterSellingPoints\": [\"人物卖点\"], \"genreSellingPoints\": [\"题材卖点\"], \"commercialRisks\": [\"商业化风险\"]}}",
|
|
6083
|
+
"硬规则:只基于笔记归纳;推断谨慎措辞;不虚构。",
|
|
6084
|
+
"重要:直接输出 JSON,字符串内不含换行。"
|
|
6085
|
+
].join("\n")
|
|
6086
|
+
});
|
|
6087
|
+
return sections;
|
|
6088
|
+
}
|
|
6089
|
+
/** 单章摘要(叙事记忆):供后续章节写作时保持连贯性。 */
|
|
6090
|
+
function chapterSummarySystemPrompt() {
|
|
6091
|
+
return [
|
|
6092
|
+
"你是一位网文编辑。请为下面一章写一段 120-200 字的摘要,供后续章节写作时保持连贯性。",
|
|
6093
|
+
"摘要必须包含:本章发生的关键事件、主角状态变化(境界/资源/伤势/心境)、新增的伏笔或线索、角色关系变化。",
|
|
6094
|
+
"用客观陈述句,不要评价,不要剧透式感叹。只输出摘要正文。"
|
|
6095
|
+
].join("\n");
|
|
6096
|
+
}
|
|
6097
|
+
/** 书分析/拆书:输入一本书/章节文本 → 卖点/结构/可借鉴点/风险。 */
|
|
6098
|
+
function bookAnalysisSystemPrompt() {
|
|
6099
|
+
return ["你是资深中文网文拆书编辑。分析输入的一本书或章节文本,提炼可复用的创作思路。", "严禁照搬具体作品的人名/专有设定/简介句子;只能提炼读者需求、结构手法和风险。"].join("\n");
|
|
6100
|
+
}
|
|
6101
|
+
//#endregion
|
|
6102
|
+
//#region src/prompts/facts.ts
|
|
6103
|
+
/** 摘要 + 事实(两件事一次出)。 */
|
|
6104
|
+
function summaryFactsSystemPrompt() {
|
|
6105
|
+
return [
|
|
6106
|
+
"你是一位网文编辑。请为下面一章做两件事,输出合法 JSON 对象:",
|
|
6107
|
+
"{\"summary\": \"120-200字摘要,含关键事件/主角状态变化(境界资源伤势心境)/新增伏笔线索/角色关系变化,客观陈述不评价\", \"facts\": [\"已确立事实1\", \"…3-6条\"]}",
|
|
6108
|
+
"facts 指:本章明确写出的、对后续有约束力的事实——人物当前状态、重要关系变化、地点与时间线、已落地或新增的伏笔线索、关键道具去向。",
|
|
6109
|
+
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。"
|
|
6110
|
+
].join("\n");
|
|
6111
|
+
}
|
|
6112
|
+
/**
|
|
6113
|
+
* 摘要 + 事实 + 时间线(三件事一次出)。
|
|
6114
|
+
*
|
|
6115
|
+
* @param wantTimeline - 关闭时间线时连 timeline 字段都不问,省掉输出 token
|
|
6116
|
+
* 与模型的无用工作量。
|
|
6117
|
+
*/
|
|
6118
|
+
function summaryFactsTimelineSystemPrompt(wantTimeline) {
|
|
6119
|
+
const NL = String.fromCharCode(10);
|
|
6120
|
+
return [
|
|
6121
|
+
"你是一位网文编辑。请为下面一章做三件事,输出**一个**合法 JSON 对象:",
|
|
6122
|
+
"{\"summary\": \"120-200字摘要,含关键事件/主角状态变化(境界资源伤势心境)/新增伏笔线索/角色关系变化,客观陈述不评价\",",
|
|
6123
|
+
" \"facts\": [\"已确立事实1\", \"…3-6条\"],",
|
|
6124
|
+
...wantTimeline ? [" \"timeline\": [{\"time\":\"第三日黄昏\",\"order\":1,\"place\":\"青云宗外门\",\"characters\":[\"沈青\"],\"event\":\"不超过40字\"}]"] : [],
|
|
6125
|
+
"",
|
|
6126
|
+
"facts 指:本章明确写出的、对后续有约束力的事实——人物当前状态、重要关系变化、地点与时间线、已落地或新增的伏笔线索、关键道具去向。",
|
|
6127
|
+
...wantTimeline ? [
|
|
6128
|
+
"timeline 指:只抽取正文里真实发生的场景节点(3-8 条),不要抽预告、回忆性泛泛陈述;纯粹的回忆/插叙请在 event 里注明「(回忆)」并给出其故事内时间。",
|
|
6129
|
+
"time 用正文里的说法(如「第三日黄昏」「入宗三个月后」「同日夜」);正文没写时间就填相对说法(如「紧接上一场景」)。",
|
|
6130
|
+
"order 是本章内的先后序号,从 1 递增;place 是一处一个词;characters 只列在场的角色名(用正文里的名字,不要写「主角」)。"
|
|
6131
|
+
] : [],
|
|
6132
|
+
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束,不要输出 Markdown 代码块标记。"
|
|
6133
|
+
].join(NL);
|
|
6134
|
+
}
|
|
6135
|
+
/** 事实抽取:本章正文 → 已确立事实(每行一条,3-6 条)。 */
|
|
6136
|
+
function extractFactsSystemPrompt() {
|
|
6137
|
+
return [
|
|
6138
|
+
"你是一位网文编辑。请从本章正文中抽取「已确立事实」,供后续章节保持一致。",
|
|
6139
|
+
"事实指:人物当前状态(境界/修为/伤势/资源/心境)、重要关系变化、地点与时间线、已落地或新增的伏笔线索、关键道具去向。",
|
|
6140
|
+
"要求:",
|
|
6141
|
+
"1. 只抽取本章明确写出的、对后续有约束力的内容;纯心理活动与无关细节不要。",
|
|
6142
|
+
"2. 每行一条事实,用客观陈述句,不含主观评价。",
|
|
6143
|
+
"3. 输出 3-6 条,每行一条,不要编号、不要前缀、不要解释。"
|
|
6144
|
+
].join("\n");
|
|
6145
|
+
}
|
|
6146
|
+
/** 时间线抽取:一章正文 → 故事时间线事件(时间/地点/在场角色/事件)。 */
|
|
6147
|
+
function timelineExtractSystemPrompt() {
|
|
6148
|
+
const NL = String.fromCharCode(10);
|
|
6149
|
+
return [
|
|
6150
|
+
"你是小说连续性编辑。任务:把一章正文压缩成「故事时间线」事件,供后续章节保持时间与地点的连续性。",
|
|
6151
|
+
"要求:",
|
|
6152
|
+
"1. 只抽取**正文里真实发生**的场景节点(3-8 条),不要抽取预告、回忆性的泛泛陈述;纯粹的回忆/插叙请在 event 里注明「(回忆)」并给出其故事内时间。",
|
|
6153
|
+
"2. time 用正文里的说法(如「第三日黄昏」「入宗三个月后」「同日夜」);正文没写时间就填相对说法(如「紧接上一场景」)。",
|
|
6154
|
+
"3. order 是本章内的先后序号,从 1 递增;跨章我会用章号排序,你只需要保证章内顺序正确。",
|
|
6155
|
+
"4. place 是场景地点(一处一个词,如「青云宗外门」);characters 只列**在场**的角色名(用正文里的名字,不要写「主角」)。",
|
|
6156
|
+
"5. event 一句话说清发生了什么(不超过 40 字)。",
|
|
6157
|
+
"输出必须是合法 JSON 数组,不要输出任何其他文字或 Markdown 代码块标记:",
|
|
6158
|
+
"[{\"time\":\"第三日黄昏\",\"order\":1,\"place\":\"青云宗外门\",\"characters\":[\"沈青\"],\"event\":\"沈青被外门弟子当众羞辱\"}]",
|
|
6159
|
+
"重要:字符串值内部不得包含换行符;JSON 必须在一段内完整结束。"
|
|
6160
|
+
].join("@NL@").replace("@NL@", NL);
|
|
6161
|
+
}
|
|
6162
|
+
/** 时间线复核:规则初筛之外,让模型找时序矛盾(时间倒流/瞬移/跨度不合理/季节错位)。 */
|
|
6163
|
+
function timelineReviewSystemPrompt() {
|
|
6164
|
+
const NL = String.fromCharCode(10);
|
|
6165
|
+
return [
|
|
6166
|
+
"你是小说连续性编辑,只做一件事:找**时间线**上的矛盾。",
|
|
6167
|
+
"检查项:1) 时间倒流(后一章早于前一章且没有明确插叙标注);2) 地点瞬移(同一人物无移动交代就换场景);",
|
|
6168
|
+
"3) 时间跨度与事件量明显不匹配(如「一日之内」发生跨越数月的剧情);4) 季节/年龄/节日前后矛盾;5) 回忆与现实混淆。",
|
|
6169
|
+
"只报**能在给定事件表里指认**的问题,不要臆测;没有问题就返回空数组。",
|
|
6170
|
+
"输出必须是合法 JSON 数组,不要任何其他文字:",
|
|
6171
|
+
"[{\"severity\":\"high|medium|low\",\"chapters\":[3,7],\"item\":\"问题一句话\",\"suggestion\":\"怎么修\"}]",
|
|
6172
|
+
"重要:字符串值内部不得包含换行符。"
|
|
6173
|
+
].join(NL);
|
|
6174
|
+
}
|
|
6175
|
+
//#endregion
|
|
6176
|
+
//#region src/prompts/audit.ts
|
|
6177
|
+
/** 单批质检:设定 + 事实库 + 该批章节节选 → 矛盾清单。 */
|
|
6178
|
+
function auditBatchSystemPrompt() {
|
|
6179
|
+
return [
|
|
6180
|
+
"你是一位严谨的网文连续性审校编辑。你会收到一本小说的道藏、事实库和一批章节正文节选。",
|
|
6181
|
+
"请找出这批章节中的一致性矛盾,例如:",
|
|
6182
|
+
"- 人物状态冲突:境界/修为/伤势/资源在同一章内或跨章前后矛盾。",
|
|
6183
|
+
"- 设定违背:正文与世界观规则、金手指规则、写作红线冲突。",
|
|
6184
|
+
"- 时间线错乱:事件顺序、时间跨度、地点移动不合逻辑。",
|
|
6185
|
+
"- 细节穿帮:人名/地名/物品/数字前后不一致。",
|
|
6186
|
+
"要求:",
|
|
6187
|
+
"1. 只报告有实质证据的矛盾,不要泛泛而谈写作质量问题。",
|
|
6188
|
+
"2. 每条必须定位到具体章节号。",
|
|
6189
|
+
"3. 输出必须是合法 JSON 数组,格式:[{\"chapterNo\": 章节号, \"severity\": \"high|medium|low\", \"item\": \"矛盾描述\", \"suggestion\": \"修改建议\"}]",
|
|
6190
|
+
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。"
|
|
6191
|
+
].join("\n");
|
|
6192
|
+
}
|
|
6193
|
+
/** 影响分析:一处改动 → 全书波及位置清单(设定/大纲/正文/事实库/简介)。 */
|
|
6194
|
+
function impactAnalysisSystemPrompt() {
|
|
6195
|
+
return [
|
|
6196
|
+
"你是一位网文一致性审校。作者要做一处修改,请找出这次改动会波及的所有位置(设定、大纲、已写章节正文、事实库、简介中可能因此过时或矛盾的内容)。",
|
|
6197
|
+
"输出必须是合法 JSON 数组,格式:[{\"location\": \"位置(第N章/大纲/道藏-世界规则/大世界-境界/事实库/简介)\", \"quote\": \"原文片段(20-60字)\", \"suggestion\": \"修改建议\", \"kind\": \"must|optional|note\"}]",
|
|
6198
|
+
"kind 含义:must=必须同步改否则矛盾;optional=建议改(影响观感);note=备注(如旧称保留为古称、或无需改但需知晓)。",
|
|
6199
|
+
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。"
|
|
6200
|
+
].join("\n");
|
|
6201
|
+
}
|
|
6202
|
+
//#endregion
|
|
6203
|
+
//#region src/prompts/adapt.ts
|
|
6204
|
+
/** 改编 P0 分析:system 侧只声明角色(指令主体在 user 侧,位置效应更不易被长上下文稀释)。 */
|
|
6205
|
+
function adaptationAnalyzeSystemPrompt() {
|
|
6206
|
+
return "你是改编策划分析助手。";
|
|
6207
|
+
}
|
|
6208
|
+
/** 改编 P0 分析:user 侧指令(原文设定卡片 + 可改范围矩阵)。 */
|
|
6209
|
+
function adaptationAnalyzeUser(sampleBodies) {
|
|
6210
|
+
return [
|
|
6211
|
+
"你是一位资深网文编辑兼改编策划。下面给你一部已完结/连载小说的若干章正文节选。请通读并输出该书的「原文设定卡片」与「可改范围矩阵」。",
|
|
6212
|
+
"输出合法 JSON 对象:",
|
|
6213
|
+
"{\"bookName\": \"书名\", \"outline\": \"一句话主线梗概(100字内)\", \"dimensions\": [{\"key\":\"realm\",\"title\":\"大世界\",\"mutability\":\"big|small|free|locked|visual\",\"current\":\"当前值\",\"evidence\":\"证据(出现章节/频次)\",\"candidates\":[{\"name\":\"候选体系名\",\"desc\":\"该体系一句话说明\"}],\"impact\":\"改了会影响什么\",\"risk\":\"high|medium|low\"}]}",
|
|
6214
|
+
"dimensions 至少覆盖以下维度(key/title):realm 大世界、cultivation 修为体系、protagonist 主角、goldenFinger 金手指、supporting 配角与势力人物名、faction 势力/组织、style 文风与叙事、ending 结局走向、timeline 时间线/编年、foreshadow 伏笔/暗线。",
|
|
6215
|
+
"mutability 取值:locked=建议保留、big=可改影响大、small=可改影响小、free=可自由改、visual=仅视觉包装。",
|
|
6216
|
+
"每个 dimension.current 必须忠于文本,能引用原文就用原文(尤其角色名/境界名/势力名/金手指名)。",
|
|
6217
|
+
"重要:所有字符串值内部不得包含换行符;JSON 必须在一段内完整结束;直接输出 JSON,不要 Markdown 代码块。"
|
|
6218
|
+
].join("\n") + "\n\n" + sampleBodies.join("\n\n---\n\n");
|
|
6219
|
+
}
|
|
6220
|
+
/** 改编方案:system 侧角色声明(指令主体在 user 侧)。 */
|
|
6221
|
+
function adaptationProposeSystemPrompt() {
|
|
6222
|
+
return "你是改编策划。";
|
|
6223
|
+
}
|
|
6224
|
+
/** 改编方案:user 侧指令 + 用户勾选的改动与已分析的可改矩阵。 */
|
|
6225
|
+
function adaptationProposeUser(selLines, dimLines) {
|
|
6226
|
+
return [
|
|
6227
|
+
"下面给出改编决策:用户想改哪些维度、改成什么值。请生成一份可执行的「改编方案」。",
|
|
6228
|
+
"要求输出合法 JSON 对象:",
|
|
6229
|
+
"{\"mappings\": [{\"source\":\"原值\",\"target\":\"新值\",\"scope\":\"name|realm|faction|term|other\",\"note\":\"说明\"}], \"rules\": {\"preserve\":[\"必须保留的要素\"],\"change\":[\"允许改变的要素\"],\"constraints\":[\"改编红线/一致性要求\"]}, \"impacts\": [{\"item\":\"受影响项\",\"detail\":\"说明\",\"risk\":\"high|medium|low\",\"chapters\":[章号]}]}",
|
|
6230
|
+
"mappings 需把用户确认的新值展开为「原→新」条目(如主角名/境界名/势力名/术语),并补充用户未填但关联的必改项(如改了修为体系名,相关的境界名一并列映射)。",
|
|
6231
|
+
"rules.preserve 至少包含:故事骨架、人物动机、伏笔逻辑、爽点结构。",
|
|
6232
|
+
"impacts 列出每个改动会影响的内容(术语/角色/章节/伏笔),能定位章号尽量定位;无法定位则给章节区间提示。",
|
|
6233
|
+
"重要:所有字符串值内部不得包含换行符;JSON 必须在一段内完整结束;直接输出 JSON,不要 Markdown 代码块。",
|
|
6234
|
+
"",
|
|
6235
|
+
"用户要改:",
|
|
6236
|
+
selLines,
|
|
6237
|
+
"",
|
|
6238
|
+
"原文可改矩阵(已分析):",
|
|
6239
|
+
dimLines
|
|
6240
|
+
].join("\n");
|
|
6241
|
+
}
|
|
6242
|
+
/** 改编重写:按映射表与规则把一章正文结构性重写(不只是换词)。 */
|
|
6243
|
+
function adaptationRewriteSystemPrompt() {
|
|
6244
|
+
return [
|
|
6245
|
+
"你是一位资深网文改编编剧。你会收到某一章的正文,以及本书的改编映射表与改编规则。",
|
|
6246
|
+
"任务:把这一章按改编方案**重写**成新版本(结构性改编,不只是换词)。",
|
|
6247
|
+
"要求:",
|
|
6248
|
+
"1. 严格遵守映射表(原值→新值),正文中所有源值都要替换为新值;涉及改名/改体系/改势力时,相关表述一起调整,使上下文自洽。",
|
|
6249
|
+
"2. 改编规则:必须保留的内容不得破坏(故事骨架/人物动机/伏笔逻辑/爽点结构);允许改变的内容可以放开调整;红线/一致性要求必须遵守。",
|
|
6250
|
+
"3. 若某个改动牵动叙事(如结局走向/时间线/世界观),要把这章的叙述顺势改得通顺、可信。",
|
|
6251
|
+
"4. 输出**只包含这一章重写后的正文**,不要重复标题,不要任何解释、开头或结尾。",
|
|
6252
|
+
"5. 字数与原章基本相当(允许 ±20%)。"
|
|
6253
|
+
].join("\n");
|
|
6254
|
+
}
|
|
6255
|
+
//#endregion
|
|
6256
|
+
//#region src/prompts/market.ts
|
|
6257
|
+
/** 市场雷达系统提示(分析师角色与输出纪律)。 */
|
|
6258
|
+
function marketRadarSystemPrompt() {
|
|
6259
|
+
return [
|
|
6260
|
+
"你是中文网络文学市场分析师。只分析输入中的公开榜单元数据或用户给出的市场线索,不补写作品正文,不假装知道未提供的信息。",
|
|
6261
|
+
"只根据输入归纳热门题材和市场信号;对未提供的信息不要臆测。",
|
|
6262
|
+
"重点分析:热门题材组合、主角身份、金手指机制、开篇危机、关系卖点、标题句式、拥挤套路和差异化机会。",
|
|
6263
|
+
"kind 只能使用 genre、protagonist、advantage、opening、relationship、title_pattern、opportunity、crowding,不得创造近义枚举值。",
|
|
6264
|
+
"榜单高频不等于适合照搬。机会建议必须说明读者满足点,同时避开直接复制具体作品。",
|
|
6265
|
+
"productionFoundation 的题材/推进模式优先引用「资源库」中已有的 existingId(名称一字不差);只有确无合适资产时才给出新资产(existingId 为 null)。",
|
|
6266
|
+
"creativeBrief 严禁复用榜单作品的人名、专有设定、简介句子和完整书名;只能提炼读者需求、爽点机制和结构机会,不得输出任何具体人名/作品名,一律使用身份或通用场景称谓。"
|
|
6267
|
+
].join("\n");
|
|
6268
|
+
}
|
|
6269
|
+
/** 雷达→灵感:基于市场信号/生产底座/创意简报,生成贴合市场的开书灵感。 */
|
|
6270
|
+
function marketIdeaInspirationSystemPrompt() {
|
|
6271
|
+
return ["你是中文网文开书灵感策划,擅长从市场榜单分析中提炼可开书方向。", "严禁输出具体人名/作品名;用身份/通用称谓。每个创意都要有明确钩子、题材、视角、长期兑现,且要与市场分析呼应但做差异化。"].join("\n");
|
|
6272
|
+
}
|
|
6273
|
+
/** 作品简介:面向读者的门面(卖点 + 开局钩子,不剧透)。 */
|
|
6274
|
+
function blurbSystemPrompt() {
|
|
6275
|
+
return [
|
|
6276
|
+
"你是一位网文平台编辑,擅长写抓人的作品简介。",
|
|
6277
|
+
"要求:",
|
|
6278
|
+
"1. 120-250 字,突出核心卖点(金手指/题材/爽点/人设反差),用一两句抛出开局钩子。",
|
|
6279
|
+
"2. 不剧透结局与关键反转;语气贴合题材(热血/悬疑/轻松/虐心)。",
|
|
6280
|
+
"3. 中文,直接输出简介正文,不要 Markdown、不要引号包裹、不要「简介:」前缀。"
|
|
6281
|
+
].join("\n");
|
|
6282
|
+
}
|
|
6283
|
+
//#endregion
|
|
5354
6284
|
//#region src/protocol.ts
|
|
5355
6285
|
/**
|
|
5356
6286
|
* dsh-novel-forge — shared protocol between the host half (Node) and the
|
|
@@ -5595,23 +6525,10 @@ const EXPORT_SCOPE_VALUES = [
|
|
|
5595
6525
|
* auto-review + rewrite, polish (de-AI-ify), narrative summaries, foreshadow
|
|
5596
6526
|
* tracking, project persistence, and whole-book export. Pure Node (no
|
|
5597
6527
|
* web-server dependencies), so routes stay thin and logic is testable.
|
|
6528
|
+
*
|
|
6529
|
+
* 系统提示词全部住在 src/prompts/(按域拆分 + catalog 注册表),本文件只做
|
|
6530
|
+
* 组装与调用——提示词文案的修改去对应域文件,不要在这里新起一段。
|
|
5598
6531
|
*/
|
|
5599
|
-
/**
|
|
5600
|
-
* 内容合规红线(平台硬性要求):所有书籍、所有章节无条件生效,
|
|
5601
|
-
* 优先级高于单书大纲/道藏中的任何设定与作者自定义红线。
|
|
5602
|
-
* 注入点:章节生成系统提示 + 审稿系统提示(命中即 high)。
|
|
5603
|
-
*/
|
|
5604
|
-
const COMPLIANCE_REDLINES = [
|
|
5605
|
-
"1. 不得出现反对宪法所确定的基本原则的内容。",
|
|
5606
|
-
"2. 不得出现危害国家安全、泄露国家秘密、颠覆国家政权、破坏国家统一的内容。",
|
|
5607
|
-
"3. 不得出现危害国家荣誉和利益的内容。",
|
|
5608
|
-
"4. 不得出现煽动民族仇恨、民族歧视、破坏民族团结的内容。",
|
|
5609
|
-
"5. 不得出现破坏国家宗教政策、宣扬邪教和愚昧迷信的内容(不得以真实宗教、邪教或迷信活动为背景进行宣扬)。",
|
|
5610
|
-
"6. 不得出现散布谣言、扰乱社会秩序、破坏社会稳定的内容。",
|
|
5611
|
-
"7. 不得出现淫秽色情、赌博、暴力、凶杀、恐怖或教唆犯罪的内容(网文语境:禁止露骨性描写、血腥暴力渲染、赌博教唆、犯罪手法详细教学)。",
|
|
5612
|
-
"8. 不得出现侮辱或者诽谤他人、侵害他人合法权益的内容(不得以真实人物、组织为原型进行侮辱或影射攻击)。",
|
|
5613
|
-
"9. 不得出现法律法规禁止的其他内容。"
|
|
5614
|
-
];
|
|
5615
6532
|
/** 审稿维度取值:归一化模型输出的 dimension 字段。维度清单以 reviewSystemPrompt 的九条为准。 */
|
|
5616
6533
|
const REVIEW_DIMENSION_IDS = /* @__PURE__ */ new Set([
|
|
5617
6534
|
"character",
|
|
@@ -6382,24 +7299,6 @@ function parseJsonObject(text) {
|
|
|
6382
7299
|
if (typeof value !== "object" || value === null) throw new Error("模型输出不是 JSON 对象");
|
|
6383
7300
|
return value;
|
|
6384
7301
|
}
|
|
6385
|
-
/** System prompt for story-bible extraction. */
|
|
6386
|
-
function bibleSystemPrompt() {
|
|
6387
|
-
return [
|
|
6388
|
-
"你是一位资深网文编辑兼设定架构师。你会收到一份小说大纲,请把它提炼成结构化的「道藏」,供后续写作时严格引用。",
|
|
6389
|
-
"要求:",
|
|
6390
|
-
"1. 忠于大纲,不自行发明大纲之外的设定。",
|
|
6391
|
-
"2. 角色卡覆盖大纲明确出现的角色(主角必含),每个角色给出性格标签、目标、关键关系。",
|
|
6392
|
-
"3. 世界规则覆盖力量体系、金手指机制、势力、地理等所有硬性规则,逐条列出。",
|
|
6393
|
-
"4. 红线列出大纲中明确禁止的内容(如无后宫、不圣母、无无脑碾压等)。",
|
|
6394
|
-
"5. 风格列出叙事基调、节奏、POV 等写作风格要点。",
|
|
6395
|
-
"6. 角色名必须用正文/编年录中的真实姓名;若大纲只写「主角」未点名,而已写章节或编年录中有名字,则用该真实姓名;禁止输出「主角(描述)」这类把身份塞进名字的占位名。",
|
|
6396
|
-
"输出必须是合法 JSON 对象,不要输出任何其他文字或 Markdown 代码块标记。",
|
|
6397
|
-
"重要:所有字符串值内部不得包含换行符(不要用多行字符串),JSON 必须在一段内完整结束。",
|
|
6398
|
-
"重要:直接输出 JSON 结果本身,不要把思考过程或推理内容写在输出里。",
|
|
6399
|
-
"JSON 结构:",
|
|
6400
|
-
"{\"genre\": \"题材与基调一句话\", \"worldRules\": [\"规则1\", \"规则2\", ...], \"characters\": [{\"name\": \"角色名\", \"role\": \"protagonist|supporting|antagonist|other\", \"traits\": [\"标签1\", ...], \"goals\": \"目标与动机\", \"relations\": \"关键关系\"}], \"redLines\": [\"红线1\", ...], \"style\": [\"风格1\", ...]}"
|
|
6401
|
-
].join("\n");
|
|
6402
|
-
}
|
|
6403
7302
|
/** Extract the story bible from an outline. */
|
|
6404
7303
|
async function extractBible(ctx, config, outline, project) {
|
|
6405
7304
|
const written = (project?.chapters ?? []).filter((c) => c.status !== "pending" && c.status !== "generating" && c.file !== void 0);
|
|
@@ -6452,21 +7351,6 @@ async function extractBible(ctx, config, outline, project) {
|
|
|
6452
7351
|
if (bible.worldRules.length === 0 && bible.characters.length === 0 && bible.redLines.length === 0) throw new Error("道藏生成失败:模型没有返回有效内容");
|
|
6453
7352
|
return bible;
|
|
6454
7353
|
}
|
|
6455
|
-
/** System prompt for volume planning. */
|
|
6456
|
-
function volumeSystemPrompt() {
|
|
6457
|
-
return [
|
|
6458
|
-
"你是一位资深网文总编。你会收到一份小说大纲,请把全书划分为若干「卷」(分卷),每卷有明确的剧情定位与起止章节。",
|
|
6459
|
-
"要求:",
|
|
6460
|
-
"1. 大纲已有分卷时,严格遵循大纲的分卷结构;没有时按剧情弧线合理划分(3-8 卷)。",
|
|
6461
|
-
"2. 卷定位一句话说明该卷的剧情重心。",
|
|
6462
|
-
"3. chapterStart/chapterEnd 给出该卷覆盖的章节区间(从 1 开始连续编号)。",
|
|
6463
|
-
"输出必须是合法 JSON 数组,不要输出任何其他文字:",
|
|
6464
|
-
"[{\"no\": 1, \"title\": \"卷名\", \"summary\": \"卷定位与剧情重心\", \"chapterStart\": 1, \"chapterEnd\": 80}]",
|
|
6465
|
-
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。",
|
|
6466
|
-
"重要:直接输出 JSON 结果本身,不要把思考过程或推理内容写在输出里。",
|
|
6467
|
-
renderLengthContract([["卷名", STRUCTURED_FIELD_LIMITS.chapterTitle], ["每卷定位", STRUCTURED_FIELD_LIMITS.chapterSummary]])
|
|
6468
|
-
].join("\n");
|
|
6469
|
-
}
|
|
6470
7354
|
/** Plan volumes from an outline. */
|
|
6471
7355
|
async function planVolumes(ctx, config, outline) {
|
|
6472
7356
|
const user = `请为下面这部小说划分卷:\n\n${outline}`;
|
|
@@ -6503,134 +7387,6 @@ function volumeOf(chapterNo, volumes) {
|
|
|
6503
7387
|
for (const volume of volumes) if (chapterNo >= volume.chapterStart && chapterNo <= volume.chapterEnd) return volume.no;
|
|
6504
7388
|
return volumes[volumes.length - 1]?.no ?? 0;
|
|
6505
7389
|
}
|
|
6506
|
-
/** The chapter-planning prompt template. */
|
|
6507
|
-
function planSystemPrompt(volumes) {
|
|
6508
|
-
return [
|
|
6509
|
-
"你是一位资深中文网文策划编辑,擅长把小说大纲拆解为可执行的章节计划。",
|
|
6510
|
-
"你会收到一份小说大纲。请根据大纲的设定、主线与节奏,规划出一份章节计划。",
|
|
6511
|
-
"要求:",
|
|
6512
|
-
"1. 每章必须有明确的核心剧情推进(不能只是过渡或凑字数)。",
|
|
6513
|
-
"2. 章节之间要衔接自然,前章结尾为后章埋下钩子。",
|
|
6514
|
-
"3. 严格遵循大纲的人设、金手指规则、战力体系与世界观设定,不得自行发明冲突设定。",
|
|
6515
|
-
"4. 输出必须是合法的 JSON 数组,不要输出任何其他文字或 Markdown 代码块标记。",
|
|
6516
|
-
"5. 数组每个元素格式:{\"title\": \"章节标题(10字以内,有网文感)\", \"beats\": \"结构化剧情要点(150-250字,必须包含四段,段间用换行分隔):\\n本章目标:本章要完成的核心推进;\\n剧情要点:主要情节的起承转合(2-4 句);\\n爽点/钩子:本章的爽点兑现或情绪钩子;\\n结尾钩子:本章结尾为下一章埋下的悬念\"}",
|
|
6517
|
-
"6. 每个章节对象可额外包含以下可选字段(尽量给出,缺失则跳过):tension(0-100 整数:本章张力目标,对抗/揭示/抉择/代价的强度;铺垫与日常章明显偏低,与相邻章节形成起伏)、mustAdvance(数组,本章必须推进的局面/关系/信息/风险/决策变化);mustPreserve(数组,本章必须保持不破坏的项);characterHardFacts(数组,本章不可违背的人物硬事实:身份/阵营/境界/当前位置/知情度);endingHook(字符串,本章结尾钩子要求);obligation(字符串,本章义务合约一句话)。",
|
|
6518
|
-
"重要:beats 字段内部必须使用 \\n 转义表示换行(JSON 字符串内不得有真实换行符),其余字符串值也不得包含真实换行符,JSON 必须在一段内完整结束。",
|
|
6519
|
-
"重要:直接输出 JSON 结果本身,不要把思考过程或推理内容写在输出里。",
|
|
6520
|
-
volumes !== void 0 && volumes.length > 0 ? ["\n全书分卷结构(规划章节时需落在对应卷内):"].concat(volumes.map((v) => `第${v.no}卷《${v.title}》:${v.summary}(章节 ${v.chapterStart}-${v.chapterEnd})` + (v.strategy !== void 0 && v.strategy !== "" ? `\n 卷战略:${v.strategy}` : "") + (v.pacing !== void 0 && v.pacing !== "" ? `\n 卷节奏板:${v.pacing}` : ""))).join("\n") : ""
|
|
6521
|
-
].join("\n");
|
|
6522
|
-
}
|
|
6523
|
-
/** Build the writing system prompt (bible + outline + active foreshadows).
|
|
6524
|
-
* `targetChars` 来自每章计划(规划时快照,= 设置的每章目标字数);无则退回默认 3500。
|
|
6525
|
-
* 字数区间按目标动态生成(±15%,取整到百位),避免系统提示词与设置互相冲突。
|
|
6526
|
-
* `lengthRule` 可覆盖第 1 条字数要求(整章修订/改编时按原文长度为准)。 */
|
|
6527
|
-
function writeSystemPrompt(project, targetChars, lengthRule, beforeChapter) {
|
|
6528
|
-
const bible = project.bible;
|
|
6529
|
-
const sections = [];
|
|
6530
|
-
if (bible !== void 0) {
|
|
6531
|
-
sections.push("==================== 道藏(写作时严格遵守) ====================");
|
|
6532
|
-
if (bible.genre !== "") sections.push(`题材基调:${bible.genre}`);
|
|
6533
|
-
if (bible.worldRules.length > 0) sections.push("世界规则:\n" + bible.worldRules.map((r) => `- ${r}`).join("\n"));
|
|
6534
|
-
const roleLib = project.roles ?? [];
|
|
6535
|
-
const labelName = {
|
|
6536
|
-
protagonist: "主角",
|
|
6537
|
-
female_lead: "女主",
|
|
6538
|
-
female_support: "女配",
|
|
6539
|
-
support: "配角",
|
|
6540
|
-
antagonist: "反派",
|
|
6541
|
-
extra: "路人"
|
|
6542
|
-
};
|
|
6543
|
-
const seenRole = /* @__PURE__ */ new Set();
|
|
6544
|
-
const mergedRoles = [];
|
|
6545
|
-
for (const r of roleLib) {
|
|
6546
|
-
seenRole.add(r.name);
|
|
6547
|
-
const card = bible.characters.find((c) => c.name === r.name);
|
|
6548
|
-
const traits = card !== void 0 ? card.traits : Array.isArray(r.traits) ? r.traits : [];
|
|
6549
|
-
const goals = card !== void 0 && card.goals !== "" ? card.goals : r.goals;
|
|
6550
|
-
const relations = card !== void 0 && card.relations !== "" ? card.relations : Array.isArray(r.relations) && r.relations.length > 0 ? r.relations.join("、") : "";
|
|
6551
|
-
mergedRoles.push(`- ${r.name}(${labelName[r.roleLabel]}):${r.identity}${traits.length > 0 ? `;性格:${traits.join("、")}` : ""}${goals !== "" ? `;目标:${goals}` : ""}${relations !== "" ? `;关系:${relations}` : ""}`);
|
|
6552
|
-
if (card !== void 0 && Array.isArray(card.knowledge) && card.knowledge.length > 0) mergedRoles.push(` 已知信息(该角色知道的:${card.knowledge.join(";")};未列出的信息该角色一律不知道,不得写其知晓或提及)`);
|
|
6553
|
-
}
|
|
6554
|
-
for (const card of bible.characters) {
|
|
6555
|
-
if (seenRole.has(card.name)) continue;
|
|
6556
|
-
seenRole.add(card.name);
|
|
6557
|
-
const roleName = {
|
|
6558
|
-
protagonist: "主角",
|
|
6559
|
-
supporting: "配角",
|
|
6560
|
-
antagonist: "反派",
|
|
6561
|
-
other: "其他"
|
|
6562
|
-
}[card.role];
|
|
6563
|
-
mergedRoles.push(`- ${card.name}(${roleName}):${card.traits.join("、")}${card.goals !== "" ? `;目标:${card.goals}` : ""}${card.relations !== "" ? `;关系:${card.relations}` : ""}`);
|
|
6564
|
-
if (Array.isArray(card.knowledge) && card.knowledge.length > 0) mergedRoles.push(` 已知信息(该角色知道的:${card.knowledge.join(";")};未列出的信息该角色一律不知道,不得写其知晓或提及)`);
|
|
6565
|
-
}
|
|
6566
|
-
if (mergedRoles.length > 0) {
|
|
6567
|
-
sections.push("角色卡(角色库与道藏已合并去重):");
|
|
6568
|
-
sections.push(...mergedRoles);
|
|
6569
|
-
}
|
|
6570
|
-
if (bible.redLines.length > 0) sections.push("写作红线(违反即失败):\n" + bible.redLines.map((r) => `- ${r}`).join("\n"));
|
|
6571
|
-
if (bible.style.length > 0) sections.push("风格要求:\n" + bible.style.map((r) => `- ${r}`).join("\n"));
|
|
6572
|
-
}
|
|
6573
|
-
const worldBlock = renderWorld(project.world);
|
|
6574
|
-
if (worldBlock !== "") sections.push(worldBlock);
|
|
6575
|
-
sections.push("==================== 全书大纲 ====================");
|
|
6576
|
-
const outlineBlock = project.outline.length > 6e3 ? project.outline.slice(0, 6e3) + "\n…(大纲过长已节选,完整内容见总纲页)" : project.outline;
|
|
6577
|
-
sections.push(outlineBlock);
|
|
6578
|
-
sections.push("==================== 大纲结束 ====================");
|
|
6579
|
-
const assetsBlock = renderAllAssets(project.assets);
|
|
6580
|
-
if (assetsBlock !== "") sections.push(assetsBlock);
|
|
6581
|
-
const active = project.foreshadows.filter((f) => f.status === "planted" || f.status === "progressing");
|
|
6582
|
-
if (active.length > 0) {
|
|
6583
|
-
sections.push("==================== 活跃伏笔(近期需推进或回收的线索) ====================");
|
|
6584
|
-
for (const f of active) sections.push(`- [${f.status === "planted" ? "已埋设" : "推进中"}] ${f.description}${f.targetChapter !== void 0 ? `(预计 ${f.targetChapter} 章回收)` : ""}`);
|
|
6585
|
-
}
|
|
6586
|
-
const lines = (project.plotlines ?? []).filter((l) => l.status === "active" || l.status === "paused");
|
|
6587
|
-
if (lines.length > 0) {
|
|
6588
|
-
const kindName = {
|
|
6589
|
-
main: "主线",
|
|
6590
|
-
branch: "支线",
|
|
6591
|
-
character: "人物线",
|
|
6592
|
-
mystery: "悬念线"
|
|
6593
|
-
};
|
|
6594
|
-
sections.push("==================== 剧情线(本章应推进至少一条活跃线) ====================");
|
|
6595
|
-
for (const l of lines) sections.push(`- [${kindName[l.kind]}${l.status === "paused" ? "·暂停中" : ""}] ${l.name}:${l.goal}${l.progress !== "" ? `(当前进度:${l.progress})` : ""}`);
|
|
6596
|
-
}
|
|
6597
|
-
sections.push("");
|
|
6598
|
-
sections.push("写作硬性要求:");
|
|
6599
|
-
const target = targetChars !== void 0 && targetChars > 0 ? targetChars : 3500;
|
|
6600
|
-
const lo = Math.max(1e3, Math.round(target * .85 / 100) * 100);
|
|
6601
|
-
const hi = Math.max(lo + 100, Math.round(target * 1.15 / 100) * 100);
|
|
6602
|
-
sections.push(lengthRule ?? `1. 每章 ${lo}-${hi} 字(目标 ${target} 字,按中文字符计),只输出章节正文,不要输出标题、章回名、作者的话或任何 Markdown 标记。`);
|
|
6603
|
-
sections.push("2. 以主角视角展开,动作、对话、心理描写交替推进,禁止大段设定说明。");
|
|
6604
|
-
sections.push("3. 尊重大纲与道藏:人设不崩、金手指规则不自相矛盾、战力不随意膨胀。");
|
|
6605
|
-
sections.push("4. 章末留一个钩子(悬念、反转或新线索),吸引读者读下一章。");
|
|
6606
|
-
sections.push("5. 语言流畅自然,符合中文网文语感,避免翻译腔与病句。");
|
|
6607
|
-
sections.push("6. 对话与冲突密度:每章至少 1 处实质对话或正面对抗/交锋场面;推理与心理活动必须用动作、环境细节、微表情、对话呈现,禁止整章纯内心独白铺陈(禁止\"解说式\"交代线索)。");
|
|
6608
|
-
sections.push("7. 反派与对手的行动力:本章出现的反派/对手必须有其行动、反制或压迫感(布局、试探、追索、交锋至少占其一),不得作为纯背景板存在。");
|
|
6609
|
-
sections.push("8. 配角辨识度:重要新登场配角应给姓名或可辨识的独有特征;禁止通篇用\"瘦高个/灰衣人/戴面具者\"等身形标签代称同一角色。");
|
|
6610
|
-
sections.push("9. 信息呈现方式:关键线索、设定、局势通过对话、动作、发现物呈现,禁止主角内心\"讲解\"给读者听。");
|
|
6611
|
-
sections.push("");
|
|
6612
|
-
sections.push("==================== 内容合规红线(平台硬性要求,最高优先级,违反即失败) ====================");
|
|
6613
|
-
sections.push(COMPLIANCE_REDLINES.join("\n"));
|
|
6614
|
-
sections.push("以上九条为硬性底线,任何情况下不得以任何形式出现或影射;若剧情确需涉及(如批判、反讽),只能以明确否定、揭露、批判的立场呈现,且不得展开细节。");
|
|
6615
|
-
sections.push(renderChapterWriterSkeleton({
|
|
6616
|
-
targetChars: target,
|
|
6617
|
-
minChars: lo,
|
|
6618
|
-
maxChars: hi,
|
|
6619
|
-
pov: "第三人称有限视角,严格跟随主角所见所知。",
|
|
6620
|
-
endingHookPreference: "章末留一个明确钩子(新信息、新风险或未闭合选择)。",
|
|
6621
|
-
tonePreference: "动作、对话、心理交替推进;重大信息用对话/动作/发现呈现。",
|
|
6622
|
-
antiAiRules: "严格遵循上方「反 AI 规则」与写法资产,避免套话与 AI 腔。"
|
|
6623
|
-
}));
|
|
6624
|
-
const timelineBlock = renderTimelineBlock(project, beforeChapter ?? Number.MAX_SAFE_INTEGER);
|
|
6625
|
-
if (timelineBlock !== "") sections.push(timelineBlock);
|
|
6626
|
-
if (beforeChapter !== void 0) {
|
|
6627
|
-
const tensionBlock = renderTensionBlock(project, beforeChapter);
|
|
6628
|
-
if (tensionBlock !== "") sections.push(tensionBlock);
|
|
6629
|
-
}
|
|
6630
|
-
const slotBlock = renderPromptSlots(project);
|
|
6631
|
-
if (slotBlock !== "") sections.push(slotBlock);
|
|
6632
|
-
return sections.join("\n");
|
|
6633
|
-
}
|
|
6634
7390
|
/**
|
|
6635
7391
|
* Plan chapters from an outline (optionally for one volume).
|
|
6636
7392
|
*/
|
|
@@ -6706,7 +7462,7 @@ async function planChapters(ctx, config, project, chapterCount, volumeNo, output
|
|
|
6706
7462
|
`请规划 ${chapterCount} 章。输出 JSON 数组(不要输出其他文字):`
|
|
6707
7463
|
].join("\n");
|
|
6708
7464
|
const parsed = await completeJsonArray(ctx, config, {
|
|
6709
|
-
system: planSystemPrompt(project.volumes) + (continuation ?
|
|
7465
|
+
system: planSystemPrompt(project.volumes) + (continuation ? planContinuationSuffix() : ""),
|
|
6710
7466
|
user,
|
|
6711
7467
|
temperature: .7,
|
|
6712
7468
|
maxTokens: resolveOutputBudget("chapter-list", { chapters: chapterCount }),
|
|
@@ -6780,49 +7536,6 @@ function retrieveKnowledge(project, query, topN = 3) {
|
|
|
6780
7536
|
if (top.length === 0) return "";
|
|
6781
7537
|
return "【书内知识库参考(与本章相关,写作时须遵守/可引用)】" + (hits.length > top.length ? `(命中 ${hits.length} 条,此处只注入最相关的 ${top.length} 条)` : "") + "\n" + top.map((x) => `- 《${x.d.title}》:${x.d.content.slice(0, 300)}`).join("\n");
|
|
6782
7538
|
}
|
|
6783
|
-
/** The review system prompt. */
|
|
6784
|
-
function reviewSystemPrompt(project) {
|
|
6785
|
-
const bible = project.bible;
|
|
6786
|
-
const sections = [
|
|
6787
|
-
"你是一位严格的网文审稿编辑。你会收到一章正文以及本书的道藏与红线。",
|
|
6788
|
-
"请从以下维度审查本章:",
|
|
6789
|
-
"1. 人设一致性:角色行为是否符合下方角色卡的设定(性格/目标/知情度/说话方式)。",
|
|
6790
|
-
"2. 设定一致性:金手指规则、战力体系、世界观是否与道藏冲突。",
|
|
6791
|
-
"3. 红线检查:是否触犯下方「本书红线」与「内容合规红线」。",
|
|
6792
|
-
"4. 文笔质量:语病、翻译腔、AI 套话(\"不禁\"\"仿佛\"\"一时间\"等高频词滥用)、流水账。",
|
|
6793
|
-
"5. 节奏与爽点:本章是否有推进、有钩子,是否拖沓灌水。",
|
|
6794
|
-
"6. 逻辑漏洞:前后矛盾、时间线错误、对话失真。",
|
|
6795
|
-
"7. 反 AI 规则:下方「本地 AI 味扫描」里已给出**本轮确定性命中的规则与次数**,直接以它为事实依据判级(禁止类命中即列为问题;鼓励类只作低优先级建议、不阻塞通过)。未列出的规则说明本章无确定性命中,不必逐条复述,也不要凭印象补报。",
|
|
6796
|
-
"8. 呈现方式:整章是否纯内心推理铺陈(无对话/无对抗,推理全靠解说);反派是否纯背景板无行动;重要配角是否无名标签化(瘦高个/灰衣人全程代称)——命中即列为问题。",
|
|
6797
|
-
"9. 内容合规(最高优先级):逐条核对下方「内容合规红线」,任何一条命中(含影射、暗示、详细描写)必须列为 high,并给出改写建议。",
|
|
6798
|
-
"输出必须是合法 JSON 对象,不要输出任何其他文字:",
|
|
6799
|
-
"{\"score\": 0-100的整数, \"riskScore\": 0-100的整数(越高越需人工处理,可结合本地AI味指数), \"tension\": 0-100的整数(本章张力强度:对抗/揭示/抉择/代价;铺垫与日常章应明显偏低), \"verdict\": \"一句话总评\", \"issues\": [{\"severity\": \"high|medium|low\", \"dimension\": \"character|setting|redline|writing|pacing|logic|anti-ai|presentation|compliance\", \"item\": \"问题描述\", \"suggestion\": \"修改建议\", \"ruleName\": \"命中的反AI规则名(见反AI规则清单)\", \"ruleType\": \"forbidden|risk|encourage\", \"category\": \"套话|句式|段落|心理|设定|节奏|对话|其他\", \"excerpt\": \"命中的原文摘录(不超过50字)\", \"reason\": \"判定理由\", \"canAutoRewrite\": true|false}]}",
|
|
6800
|
-
"tension 是本章张力强度评分(不是质量分):对决/揭秘/生死抉择偏高,铺垫章、日常章、过渡章偏低;同一本书内保持同一尺度。",
|
|
6801
|
-
"反 AI 类 issue 尽量给出 ruleName/ruleType/category/excerpt/reason/canAutoRewrite,便于统计与自动改写。",
|
|
6802
|
-
"风格符合度:若下方给出「风格指纹比对」,以它为准判断本章是否偏离绑定写法——偏差显著时按 writing 维度、category 用「风格」上报;比对里说没有显著偏差时,不要报风格问题。",
|
|
6803
|
-
"AI 套话高频模板词示例(集中出现必须整体降密度):仿佛、似乎、极其、完美、深不见底、形成了、莫名、无法形容、难以言喻、精心雕琢、肤光胜雪、眉目如画、歌舞升平、觥筹交错、妙语连珠、不可名状、另一层真相、命运、真相。",
|
|
6804
|
-
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。",
|
|
6805
|
-
"重要:直接输出 JSON 结果本身,不要把思考过程写在输出里。"
|
|
6806
|
-
];
|
|
6807
|
-
const assetsBlock = renderAllAssets(project.assets);
|
|
6808
|
-
if (assetsBlock !== "") sections.push("\n" + assetsBlock);
|
|
6809
|
-
if (bible !== void 0) {
|
|
6810
|
-
sections.push("\n==================== 道藏 ====================");
|
|
6811
|
-
if (bible.worldRules.length > 0) sections.push("世界规则:\n" + bible.worldRules.map((r) => `- ${r}`).join("\n"));
|
|
6812
|
-
if (bible.characters.length > 0) {
|
|
6813
|
-
sections.push("角色卡:");
|
|
6814
|
-
for (const card of bible.characters) {
|
|
6815
|
-
sections.push(`- ${card.name}(${card.role}):${card.traits.join("、")}`);
|
|
6816
|
-
if (Array.isArray(card.knowledge) && card.knowledge.length > 0) sections.push(` 该角色知道:${card.knowledge.join(";")}(未列出的信息该角色不知道)`);
|
|
6817
|
-
}
|
|
6818
|
-
}
|
|
6819
|
-
if (bible.redLines.length > 0) sections.push("红线:\n" + bible.redLines.map((r) => `- ${r}`).join("\n"));
|
|
6820
|
-
}
|
|
6821
|
-
sections.push("\n==================== 内容合规红线(平台硬性要求,最高优先级) ====================");
|
|
6822
|
-
sections.push(COMPLIANCE_REDLINES.join("\n"));
|
|
6823
|
-
sections.push("以上九条为硬性底线:正文中任何一条命中(含影射、暗示、详细展开)都必须列为 high,并给出改写建议;作者自定义红线不得豁免这九条。");
|
|
6824
|
-
return sections.join("\n");
|
|
6825
|
-
}
|
|
6826
7539
|
/**
|
|
6827
7540
|
* 风格基线的取样上限与取法。
|
|
6828
7541
|
*
|
|
@@ -7014,82 +7727,12 @@ async function reviewChapterText(ctx, config, project, text, previousReport) {
|
|
|
7014
7727
|
score,
|
|
7015
7728
|
passed,
|
|
7016
7729
|
verdict: typeof raw.verdict === "string" ? raw.verdict.slice(0, 200) : "",
|
|
7017
|
-
issues,
|
|
7018
|
-
riskScore,
|
|
7019
|
-
aiFlavor: aiScan.aiScore,
|
|
7020
|
-
aiPhrases: aiScan.clicheHits.slice(0, 8),
|
|
7021
|
-
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7022
|
-
};
|
|
7023
|
-
}
|
|
7024
|
-
/** 验证模式系统提示:修订后逐条核对原意见是否解决,只挑新增 high,不重复挑剔主观项。 */
|
|
7025
|
-
function verifySystemPrompt(project) {
|
|
7026
|
-
return [
|
|
7027
|
-
"你是一位网文审稿验证员。作者已按上一轮审稿意见修订了本章,你需要验证修订效果。",
|
|
7028
|
-
"你的任务(严格按此执行):",
|
|
7029
|
-
"1. 逐条核对「上一轮意见」中的每一条(按编号 1、2、3...)是否已在修订稿中解决。",
|
|
7030
|
-
"2. 只挑修订【新引入】的 high 级问题(设定矛盾/逻辑硬伤/事实错误)——新引入的 medium/low 主观项(文笔/套话/节奏)不要列。",
|
|
7031
|
-
"3. 禁止重复挑剔上一轮已指出且本次已解决的主观项(如\"缓缓/微微\"等套话、错别字)——即使换个说法再提也不行。",
|
|
7032
|
-
"4. 严禁为了显得专业而新增\"换一批毛病\"式的意见。",
|
|
7033
|
-
"输出必须是合法 JSON 对象,包含以下字段:",
|
|
7034
|
-
"- resolvedIds:已解决的上一轮意见编号数组(如 [1, 3, 5])。",
|
|
7035
|
-
"- unresolvedIds:未解决或部分解决的上一轮意见编号数组(如 [2, 4])。",
|
|
7036
|
-
"- issues:未解决的原意见 + 新引入的 high 级问题列表(格式同审稿:severity/item/suggestion)。未解决的原意见 item 需注明\"未解决(编号N):原意见摘要\"。",
|
|
7037
|
-
"- score:按修订稿整体质量给 50-90 分(解决全部 high 且无新增 high 时给 70 以上)。",
|
|
7038
|
-
"- verdict:一句话结论。",
|
|
7039
|
-
"完整格式:{\"resolvedIds\": [1,3], \"unresolvedIds\": [2], \"score\": 75, \"verdict\": \"一句话\", \"issues\": [{\"severity\": \"high\", \"dimension\": \"character|setting|redline|writing|pacing|logic|anti-ai|presentation|compliance\", \"item\": \"未解决(2):xxx\", \"suggestion\": \"xxx\"}]}",
|
|
7040
|
-
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。",
|
|
7041
|
-
"重要:直接输出 JSON 结果本身,不要把思考过程写在输出里。",
|
|
7042
|
-
renderBibleForVerify(project)
|
|
7043
|
-
].join("\n");
|
|
7044
|
-
}
|
|
7045
|
-
/**
|
|
7046
|
-
* 验证模式下注入道藏。
|
|
7047
|
-
*
|
|
7048
|
-
* 原实现是 `JSON.stringify(project.bible).slice(0, 3000)`——有两个问题:
|
|
7049
|
-
* 1. **腰斩的 JSON 不是 JSON**:`slice` 从中间切断,审稿员会看到语法残缺的
|
|
7050
|
-
* 对象(字符串没闭合、数组没结尾),既浪费 token 又可能误读设定。
|
|
7051
|
-
* 2. **静默截断设定**:世界规则/角色卡排在后面的字段最先被切掉,而那恰恰是
|
|
7052
|
-
* 核对设定冲突最需要的部分。
|
|
7053
|
-
*
|
|
7054
|
-
* 改为结构化渲染 + 按「世界规则/角色/红线」分配独立预算。
|
|
7055
|
-
*/
|
|
7056
|
-
function renderBibleForVerify(project) {
|
|
7057
|
-
const bible = project.bible;
|
|
7058
|
-
if (bible === void 0) return "本书道藏(核对设定冲突用):\n(无)";
|
|
7059
|
-
const parts = [];
|
|
7060
|
-
if (bible.worldRules.length > 0) parts.push(`世界规则:\n${bible.worldRules.map((r) => `- ${r}`).join("\n")}`);
|
|
7061
|
-
if (bible.characters.length > 0) parts.push(`角色卡:\n${bible.characters.map((c) => {
|
|
7062
|
-
const goals = c.goals !== "" ? `;目标:${c.goals}` : "";
|
|
7063
|
-
const rel = c.relations !== "" ? `;关系:${c.relations}` : "";
|
|
7064
|
-
return `- ${c.name}:${c.traits.join("、")}${goals}${rel}`;
|
|
7065
|
-
}).join("\n")}`);
|
|
7066
|
-
if (bible.redLines.length > 0) parts.push(`红线:\n${bible.redLines.map((r) => `- ${r}`).join("\n")}`);
|
|
7067
|
-
if (parts.length === 0) return "本书道藏(核对设定冲突用):\n(无)";
|
|
7068
|
-
const block = parts.join("\n");
|
|
7069
|
-
const limit = 6e3;
|
|
7070
|
-
return `本书道藏(核对设定冲突用):\n${block.length > limit ? block.slice(0, limit) + `\n…(道藏过长已节选,但角色名/世界规则已尽量保留;如需完整设定请查总纲页)` : block}`;
|
|
7071
|
-
}
|
|
7072
|
-
/** Build the author-review system prompt (narrative structure, not prose). */
|
|
7073
|
-
function authorReviewSystemPrompt() {
|
|
7074
|
-
return [
|
|
7075
|
-
"你是一位网文作者复盘助手。你会收到:本章正文、上一章结尾(钩子)、上一章作者复盘(如有)、活跃剧情线与编年录近期事实。",
|
|
7076
|
-
"请从叙事结构层面复盘本章(不评文笔,那是审稿的事):",
|
|
7077
|
-
"1. hookHonored:上一章结尾的钩子/悬念是否在本章兑现或推进(true/false)。",
|
|
7078
|
-
"2. hookNote:钩子兑现情况一句话;未兑现时说明并给出\"建议在第几章补\"的建议。",
|
|
7079
|
-
"3. endingHook:本章结尾钩子强度,0-10 的整数(低于 6 说明结尾平淡,读者可能不想看下一章)。",
|
|
7080
|
-
"4. plotlineProgress:本章推进了哪条剧情线(主线/支线名),或\"无实质推进\"(连续无推进要提醒)。",
|
|
7081
|
-
"5. advancedLines:本章实际推进的剧情线名称数组——从「活跃剧情线」清单中选出推进了的线(名称必须与清单中的线名一字不差;没推进任何线则输出空数组)。",
|
|
7082
|
-
"6. continuity:与上一章结尾的衔接检查(人物位置/时间/伤势/资源/对话状态),发现问题要指出。",
|
|
7083
|
-
"7. trend:结合上一章复盘看近期节奏趋势(是否连续拖沓、爽点密度是否下降、是否需要调整)。",
|
|
7084
|
-
"8. stateChanges:本章发生的关键状态变化数组(人物状态/世界局面/关系/资源,每条约 20 字,最多 6 条),供整本与事实库回灌。",
|
|
7085
|
-
"9. newConflicts:本章新引入或明显升级的冲突数组(最多 4 条)。",
|
|
7086
|
-
"10. clues:本章埋下或推进的新线索/伏笔数组(最多 4 条)。",
|
|
7087
|
-
"11. absentRisks:本章缺席但按卷级职责应出场/该推进关系的重要角色,及其缺席带来的风险(最多 4 条)。",
|
|
7088
|
-
"输出必须是合法 JSON 对象,不要输出任何其他文字:",
|
|
7089
|
-
"{\"hookHonored\": true或false, \"hookNote\": \"一句话\", \"endingHook\": 0-10整数, \"plotlineProgress\": \"一句话\", \"advancedLines\": [\"线名\"], \"continuity\": \"一句话\", \"trend\": \"一句话\", \"stateChanges\": [\"状态变化\"], \"newConflicts\": [\"新冲突\"], \"clues\": [\"新线索\"], \"absentRisks\": [\"本章缺席但值得注意的角色及风险(卷级职责/缺席风险,最多4条)\"]}",
|
|
7090
|
-
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。",
|
|
7091
|
-
"重要:直接输出 JSON 结果本身,不要把思考过程写在输出里。"
|
|
7092
|
-
].join("\n");
|
|
7730
|
+
issues,
|
|
7731
|
+
riskScore,
|
|
7732
|
+
aiFlavor: aiScan.aiScore,
|
|
7733
|
+
aiPhrases: aiScan.clicheHits.slice(0, 8),
|
|
7734
|
+
reviewedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7735
|
+
};
|
|
7093
7736
|
}
|
|
7094
7737
|
/** 作者复盘:对一章做叙事结构复盘(钩子兑现/结尾钩子/推进/连续性/趋势)。 */
|
|
7095
7738
|
async function authorReviewChapter(ctx, config, project, chapterNo, body, prevTail) {
|
|
@@ -7140,14 +7783,7 @@ function autoLinkPlotlines(project, chapterNo, advancedLines) {
|
|
|
7140
7783
|
}
|
|
7141
7784
|
/** 建议剧情线:基于大纲/卷计划/已写章节/编年录,提炼候选线。 */
|
|
7142
7785
|
async function suggestPlotlines(ctx, config, project) {
|
|
7143
|
-
const system =
|
|
7144
|
-
"你是一位网文剧情架构师。根据本书的大纲、卷计划、已写章节标题与编年录,为作者提炼建议的剧情线(主线/支线/人物线/悬念线)。",
|
|
7145
|
-
"每条线要:名称简洁有力;目标写清楚这条线最终要完成什么;progress 写当前推进到哪(没有就空字符串)。",
|
|
7146
|
-
"建议 4-8 条,覆盖:1 条主线、1-2 条人物线、1-2 条悬念线、1-3 条支线。避免与大纲明显重复的废话线。",
|
|
7147
|
-
"输出必须是合法 JSON 数组,格式:[{\"name\": \"线名\", \"kind\": \"main|branch|character|mystery\", \"goal\": \"目标\", \"progress\": \"\"}]",
|
|
7148
|
-
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。",
|
|
7149
|
-
"重要:直接输出 JSON 结果本身,不要把思考过程写在输出里。"
|
|
7150
|
-
].join("\n");
|
|
7786
|
+
const system = suggestPlotlinesSystemPrompt();
|
|
7151
7787
|
const written = project.chapters.filter((c) => c.status !== "pending");
|
|
7152
7788
|
const raw = parseJsonArray(await complete(ctx, config, {
|
|
7153
7789
|
system,
|
|
@@ -7198,12 +7834,7 @@ async function refreshPlotlineProgress(ctx, config, project, target) {
|
|
|
7198
7834
|
}
|
|
7199
7835
|
/** 实际的批量刷新实现(被上面的重载收敛到一处)。 */
|
|
7200
7836
|
async function refreshLinesFor(ctx, config, project, lines) {
|
|
7201
|
-
const system =
|
|
7202
|
-
lines.length === 1 ? "你是一位网文剧情线管理员。请根据「剧情线信息」与「本书已写章节摘要/编年录」,判断这条线目前推进到了哪一步。" : `你是一位网文剧情线管理员。下面给了 ${lines.length} 条剧情线,请**逐条**判断各自推进到了哪一步。`,
|
|
7203
|
-
"每条线输出一句话(30-60 字):该线当前的状态、最近一次推进发生在第几章、下一步可能的方向。还没开始推进的线,明确说\"尚未推进\"。",
|
|
7204
|
-
lines.length === 1 ? "输出必须是合法 JSON 对象:{\"progress\": \"一句话\"}" : `输出必须是合法 JSON 对象:{"items": [{"name": "线名(与输入完全一致)", "progress": "一句话"}]},items 的条数必须等于 ${lines.length},name 必须与输入里的线名逐字相同(用于对齐)`,
|
|
7205
|
-
"重要:不要输出任何其他文字。"
|
|
7206
|
-
].join("\n");
|
|
7837
|
+
const system = refreshPlotlinesSystemPrompt(lines.length);
|
|
7207
7838
|
const sharedContext = [`章节摘要(最近 8 章):\n${project.chapters.filter((c) => c.status !== "pending" && c.summary !== void 0 && c.summary !== "").slice(-8).map((c) => `第${c.no}章《${c.title}》:${c.summary.slice(0, 120)}`).join("\n")}`, (project.facts ?? []).length > 0 ? `编年录近期事实(最近 15 条):\n${(project.facts ?? []).slice(-15).map((f) => `[第${f.chapterNo}章] ${f.text.slice(0, 100)}`).join("\n")}` : ""];
|
|
7208
7839
|
const describe = (l) => [
|
|
7209
7840
|
`剧情线:${l.name}(${l.kind})`,
|
|
@@ -7238,28 +7869,7 @@ async function refreshLinesFor(ctx, config, project, lines) {
|
|
|
7238
7869
|
}
|
|
7239
7870
|
/** AI 从全书提炼角色库:大纲 + 道藏 + 编年录 + 章节摘要 → 结构化角色清单。 */
|
|
7240
7871
|
async function extractRoles(ctx, config, project) {
|
|
7241
|
-
const system =
|
|
7242
|
-
"你是一位网文角色库管理员。请根据本书的大纲、设定、编年录与章节摘要,提炼完整的角色库。",
|
|
7243
|
-
"覆盖原则:所有在编年录/章节中实际出场或有名有姓的角色都应收录;无名的功能性人物(如\"矮胖姑娘\")用其身份简称收录并标注;反复出现且有剧情作用的身份型角色(站长、律师、警察、法官、店主等)必须收录。",
|
|
7244
|
-
"数量控制:最多输出 20 个角色;覆盖优先——主角、主要反派、重要配角(女主/关键配角)必须全收,所有有名有姓者必收;只有真正一次性路人(无名字、无剧情作用)才可省略。",
|
|
7245
|
-
"重要:正常一部完整故事应提炼 8-20 个角色;若少于 6 个通常说明漏提炼,必须重新核对正文摘录。",
|
|
7246
|
-
"重要:正文中若有明确的主角与主要反派,必须分别以 protagonist / antagonist 收录,禁止遗漏;反派确实未出场时才可省略。",
|
|
7247
|
-
"输出优先级:主角(protagonist)与主要反派(antagonist)必须优先输出并完整刻画,其次女主/重要配角;判断不出名字时用正文中的身份称呼。",
|
|
7248
|
-
"每个角色输出:",
|
|
7249
|
-
"1. name:角色名(或身份简称)。主角/有名配角必须用正文中实际出现的人名(如「沈放」),禁止用「主角(38岁超市理货员)」这类把身份塞进名字的占位名;正文确实没点名时才可用身份简称(如「富商」「灰衣老人」)。",
|
|
7250
|
-
"2. roleLabel:定位——protagonist=主角;female_lead=女主(唯一知己/感情线核心,无后宫前提下只此一位);female_support=重要女配;support=普通配角;antagonist=反派;extra=路人/背景。",
|
|
7251
|
-
"3. identity:身份一句话(宗门/势力/血脉/职业)。",
|
|
7252
|
-
"4. traits:3-6 个性格标签。",
|
|
7253
|
-
"5. goals:目标与动机一句话。",
|
|
7254
|
-
"6. relations:关系网数组,格式[\"角色名(关系)\", ...]。",
|
|
7255
|
-
"7. arc:成长线数组,格式[\"阶段:说明\", ...](如\"出场:祭品身份\"/\"转折:祭祀被中断脱身\")。",
|
|
7256
|
-
"8. knowledge:该角色已经知道的关键信息(3-8 条),不知道的信息不要写进去。",
|
|
7257
|
-
"精简要求:identity 控制在 30 字内;traits 3-6 个短标签;goals 60 字内;relations 2-5 条;arc 2-4 条;knowledge 每条 40 字内。整体输出量要紧凑,避免冗长。",
|
|
7258
|
-
"重要:用户消息里列出的「已收录角色」绝不要再次输出——这些角色已经在角色库里,跳过它们,只提炼未收录的。",
|
|
7259
|
-
"输出必须是合法 JSON 数组,不要输出其他文字:[{\"name\":\"...\", \"roleLabel\":\"...\", \"identity\":\"...\", \"traits\":[...], \"goals\":\"...\", \"relations\":[...], \"arc\":[...], \"knowledge\":[...]}]",
|
|
7260
|
-
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。",
|
|
7261
|
-
"重要:直接输出 JSON 结果本身,不要把思考过程写在输出里。"
|
|
7262
|
-
].join("\n");
|
|
7872
|
+
const system = extractRolesSystemPrompt();
|
|
7263
7873
|
const written = project.chapters.filter((c) => c.status !== "pending" && c.status !== "generating");
|
|
7264
7874
|
const existingRoles = project.roles ?? [];
|
|
7265
7875
|
const sampleChapters = written.length <= 10 ? written : (() => {
|
|
@@ -7357,8 +7967,7 @@ async function extractRoles(ctx, config, project) {
|
|
|
7357
7967
|
if (role !== void 0) roles.push(role);
|
|
7358
7968
|
}
|
|
7359
7969
|
if (roles.length > 0) {
|
|
7360
|
-
const
|
|
7361
|
-
const patchSystem = system + "\n上一次已提炼角色:" + names + "。\n现在只输出「遗漏的角色」JSON 数组:检查正文摘录中反复出现、有固定身份称呼(如站长、律师、警察、法官、店主、老师)且对剧情有作用的角色;一次性路人不要输出。没有遗漏就输出 []。字段与上面相同。";
|
|
7970
|
+
const patchSystem = system + roleCatchUpSuffix(roles.map((r) => r.name).join("、"));
|
|
7362
7971
|
try {
|
|
7363
7972
|
const patchRaw = parseJsonArray(await complete(ctx, config, {
|
|
7364
7973
|
system: patchSystem,
|
|
@@ -7634,23 +8243,7 @@ function importBookText(filePath, outputDir) {
|
|
|
7634
8243
|
async function suggestOutlines(ctx, config, idea, count = 3, exclude = []) {
|
|
7635
8244
|
const n = Math.max(1, Math.min(3, Math.floor(count)));
|
|
7636
8245
|
const parsed = parseJsonArray(await complete(ctx, config, {
|
|
7637
|
-
system:
|
|
7638
|
-
"你是一位资深网文策划。作者只给了一句「想法」,你需要把它扩展成 2-3 个【方向差异明显】的完整小说大纲方案,供作者挑选。",
|
|
7639
|
-
"每个方案必须满足:",
|
|
7640
|
-
"1. bookName:书名(6 字以内,抓眼球、点题)。",
|
|
7641
|
-
"2. genre:题材(如 仙侠修真 / 都市异能 / 玄幻 / 悬疑)。",
|
|
7642
|
-
"3. sellingPoint:核心卖点一句话(金手指/爽点/差异化,40 字内)。",
|
|
7643
|
-
"4. outline:完整大纲文本(至少 800 字,可直接作为开书大纲),结构包含:书名与题材、金手指/核心设定、主角人设与动机、主线剧情走向(至少 5 个阶段)、关键配角与势力、卖点与爽点设计、预计分卷(3-5 卷)。",
|
|
7644
|
-
"方向差异要求:",
|
|
7645
|
-
"- 方案之间的金手指/剧情走向必须明显不同(如:苟道发育流 vs 随身老爷爷流 vs 群像争霸流),不能只是换书名。",
|
|
7646
|
-
"- 忠实于作者想法的核心要素,但允许在不同方向上进行合理演绎。",
|
|
7647
|
-
"- 不输出任何与已列「需避开的方向」雷同的方案。",
|
|
7648
|
-
"输出必须是合法 JSON 数组,只输出数组本身:",
|
|
7649
|
-
"[{\"id\": \"唯一id\", \"bookName\": \"...\", \"genre\": \"...\", \"sellingPoint\": \"...\", \"outline\": \"...\"}]",
|
|
7650
|
-
`本次只输出 ${n} 个方案。`,
|
|
7651
|
-
"重要:所有字符串值内部不得包含换行符(大纲内部分段请用「。\n」或「;」自然断句),JSON 必须在一段内完整结束。",
|
|
7652
|
-
"重要:直接输出 JSON 结果本身,不要把思考过程写在输出里。"
|
|
7653
|
-
].join("\n"),
|
|
8246
|
+
system: outlineCandidatesSystemPrompt(n),
|
|
7654
8247
|
user: [
|
|
7655
8248
|
`作者的想法:${idea}`,
|
|
7656
8249
|
idea.trim().length < 40 ? "作者的想法非常简短(可能只有一句)。请基于通用网文套路合理扩展补全:为每个方案自洽地设计金手指/核心设定、主角人设与动机、主线走向,使其成为完整可开书的大纲;不同方案的方向仍须明显差异。" : "",
|
|
@@ -7712,13 +8305,7 @@ async function breakdownBook(ctx, config, project, outputDir, scope = "recent",
|
|
|
7712
8305
|
}
|
|
7713
8306
|
const notes = [];
|
|
7714
8307
|
let usedTokens = 0;
|
|
7715
|
-
const noteSystem =
|
|
7716
|
-
"你是中文网文拆书助手。把单章正文整理成结构化笔记,供后续章节级分析复用。",
|
|
7717
|
-
"只输出 JSON 对象:",
|
|
7718
|
-
"{\"summary\": \"1-2句\", \"plotPoints\": [\"...\"], \"characters\": [\"...\"], \"worldbuilding\": [\"...\"], \"styleTechniques\": [\"...\"], \"marketHighlights\": [\"...\"], \"weaknessSignals\": [\"...\"]}",
|
|
7719
|
-
"硬规则:只提取正文明确出现的信息;每数组最多 4 项;不要补写原文外的动机/意图;evidence 不在此阶段输出。",
|
|
7720
|
-
"重要:直接输出 JSON,不要输出其他文字;字符串内不含换行。"
|
|
7721
|
-
].join("\n");
|
|
8308
|
+
const noteSystem = breakdownNoteSystemPrompt();
|
|
7722
8309
|
for (const ch of chunks) {
|
|
7723
8310
|
const noteUser = [
|
|
7724
8311
|
`第${ch.no}章《${ch.title}》`,
|
|
@@ -7738,63 +8325,7 @@ async function breakdownBook(ctx, config, project, outputDir, scope = "recent",
|
|
|
7738
8325
|
usedTokens += 800;
|
|
7739
8326
|
} catch {}
|
|
7740
8327
|
}
|
|
7741
|
-
const sectionsConfig =
|
|
7742
|
-
{
|
|
7743
|
-
key: "overview",
|
|
7744
|
-
title: "拆书总览",
|
|
7745
|
-
focus: "一句话定位、题材标签、整体优势与短板",
|
|
7746
|
-
system: [
|
|
7747
|
-
"你是资深中文网文拆书分析师,负责《拆书总览》小节。",
|
|
7748
|
-
"基于给定章节笔记做低风险综合判断,输出 JSON:{\"markdown\": \"可直接展示的分析稿(简体中文,先给结论再说明体现在哪、为何成立)\", \"structured\": {\"oneLinePositioning\": \"一句话定位\", \"genreTags\": [\"题材标签\"], \"sellingPointTags\": [\"卖点标签\"], \"strengths\": [\"整体优势\"], \"weaknesses\": [\"整体短板\"]}}",
|
|
7749
|
-
"硬规则:只基于笔记归纳;推断用「更偏向/可能」等谨慎措辞;证据不足写「材料不足」;不虚构原文细节。",
|
|
7750
|
-
"重要:直接输出 JSON,字符串内不含换行。"
|
|
7751
|
-
].join("\n")
|
|
7752
|
-
},
|
|
7753
|
-
{
|
|
7754
|
-
key: "plot",
|
|
7755
|
-
title: "剧情结构",
|
|
7756
|
-
focus: "主线梗概、阶段推进、冲突升级、节奏风险",
|
|
7757
|
-
system: [
|
|
7758
|
-
"你是资深中文网文拆书分析师,负责《剧情结构》小节。",
|
|
7759
|
-
"基于给定章节笔记分析,输出 JSON:{\"markdown\": \"分析稿(简体中文,先结论后依据)\", \"structured\": {\"mainlineSummary\": \"主线梗概\", \"phaseProgressions\": [\"阶段推进\"], \"escalationDesigns\": [\"冲突升级\"], \"paceRisks\": [\"节奏风险\"], \"reusablePatterns\": [\"可复用套路\"]}}",
|
|
7760
|
-
"硬规则:只基于笔记归纳;推断谨慎措辞;不虚构。",
|
|
7761
|
-
"重要:直接输出 JSON,字符串内不含换行。"
|
|
7762
|
-
].join("\n")
|
|
7763
|
-
},
|
|
7764
|
-
{
|
|
7765
|
-
key: "character",
|
|
7766
|
-
title: "人物系统",
|
|
7767
|
-
focus: "主角定位、配角功能、关系网络、成长弧线、辨识度风险",
|
|
7768
|
-
system: [
|
|
7769
|
-
"你是资深中文网文拆书分析师,负责《人物系统》小节。",
|
|
7770
|
-
"基于给定章节笔记分析,输出 JSON:{\"markdown\": \"分析稿(简体中文,先结论后依据)\", \"structured\": {\"protagonistPositioning\": \"主角定位\", \"supportingFunctions\": [\"配角功能\"], \"relationshipNetwork\": [\"关系网络\"], \"growthArcs\": [\"成长弧线\"], \"clarityRisks\": [\"辨识度风险\"]}}",
|
|
7771
|
-
"硬规则:只基于笔记归纳;推断谨慎措辞;不虚构。",
|
|
7772
|
-
"重要:直接输出 JSON,字符串内不含换行。"
|
|
7773
|
-
].join("\n")
|
|
7774
|
-
},
|
|
7775
|
-
{
|
|
7776
|
-
key: "style",
|
|
7777
|
-
title: "文风与技法",
|
|
7778
|
-
focus: "叙事视角、语言风格、描写方式、节奏控制、钩子设计、可复用写法",
|
|
7779
|
-
system: [
|
|
7780
|
-
"你是资深中文网文拆书分析师,负责《文风与技法》小节。",
|
|
7781
|
-
"基于给定章节笔记分析,输出 JSON:{\"markdown\": \"分析稿(简体中文,先结论后依据)\", \"structured\": {\"narrativePov\": \"叙事视角\", \"languageStyle\": \"语言风格\", \"dialoguePatterns\": [\"对话特征\"], \"rhythmControl\": [\"节奏控制\"], \"hookDesigns\": [\"钩子设计\"], \"reusableTechniques\": [\"可复用写法\"]}}",
|
|
7782
|
-
"硬规则:只基于笔记归纳;推断谨慎措辞;不虚构。",
|
|
7783
|
-
"重要:直接输出 JSON,字符串内不含换行。"
|
|
7784
|
-
].join("\n")
|
|
7785
|
-
}
|
|
7786
|
-
];
|
|
7787
|
-
if (preset === "standard") sectionsConfig.push({
|
|
7788
|
-
key: "market",
|
|
7789
|
-
title: "商业化卖点",
|
|
7790
|
-
focus: "读者爽点、点击驱动、人物/题材卖点、商业化风险",
|
|
7791
|
-
system: [
|
|
7792
|
-
"你是资深中文网文拆书分析师,负责《商业化卖点》小节。",
|
|
7793
|
-
"基于给定章节笔记分析,输出 JSON:{\"markdown\": \"分析稿(简体中文,先结论后依据)\", \"structured\": {\"hookPoints\": [\"读者爽点\"], \"clickDrivers\": [\"点击驱动\"], \"characterSellingPoints\": [\"人物卖点\"], \"genreSellingPoints\": [\"题材卖点\"], \"commercialRisks\": [\"商业化风险\"]}}",
|
|
7794
|
-
"硬规则:只基于笔记归纳;推断谨慎措辞;不虚构。",
|
|
7795
|
-
"重要:直接输出 JSON,字符串内不含换行。"
|
|
7796
|
-
].join("\n")
|
|
7797
|
-
});
|
|
8328
|
+
const sectionsConfig = breakdownSections(preset);
|
|
7798
8329
|
const notesText = notes.join("\n\n");
|
|
7799
8330
|
const sections = [];
|
|
7800
8331
|
const evidence = [];
|
|
@@ -8061,18 +8592,7 @@ async function registerLlmModel(ctx, req) {
|
|
|
8061
8592
|
}
|
|
8062
8593
|
/** 剧情健康检查:基于已写章节数/各线状态/编年录,判断是否需要新线及添加时机。 */
|
|
8063
8594
|
async function analyzePlotlineHealth(ctx, config, project) {
|
|
8064
|
-
const system =
|
|
8065
|
-
"你是一位网文剧情架构师。请对本书的「剧情线体系」做健康检查,判断当前是否需要新增剧情线、应在多少章后添加。",
|
|
8066
|
-
"评估维度:各线最近推进到第几章(已写章节与关联章节的差值越大越危险)、各线状态、已写章节总数、卷计划当前进度、编年录近期事实。",
|
|
8067
|
-
"输出规则:",
|
|
8068
|
-
"1. verdict:一句话结论——\"需要新增线\" / \"暂不需要\" / \"再写 N 章后需要\"(N 给出具体章数)。",
|
|
8069
|
-
"2. timing:说明建议添加的时机(如:第 25 章前引入新支线,因为主线预计第 22 章告一段落)。",
|
|
8070
|
-
"3. reasons:3-5 条依据(引用具体数据:哪条线多少章没推进、已写章节数、卷进度等)。",
|
|
8071
|
-
"4. lines:对每条线给健康度——ok(近期推进过)/ warning(超过 5 章未推进)/ stale(超过 10 章未推进或悬置过久)。",
|
|
8072
|
-
"输出必须是合法 JSON 对象:{\"verdict\": \"...\", \"timing\": \"...\", \"reasons\": [\"...\"], \"lines\": [{\"name\": \"线名\", \"health\": \"ok|warning|stale\", \"note\": \"一句说明\"}]}",
|
|
8073
|
-
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。",
|
|
8074
|
-
"重要:直接输出 JSON 结果本身,不要把思考过程写在输出里。"
|
|
8075
|
-
].join("\n");
|
|
8595
|
+
const system = plotlineHealthSystemPrompt();
|
|
8076
8596
|
const written = project.chapters.filter((c) => c.status !== "pending" && c.status !== "generating");
|
|
8077
8597
|
const lines = (project.plotlines ?? []).filter((l) => l.status === "active" || l.status === "paused");
|
|
8078
8598
|
const raw = parseJsonObject(await complete(ctx, config, {
|
|
@@ -8107,13 +8627,7 @@ async function analyzePlotlineHealth(ctx, config, project) {
|
|
|
8107
8627
|
}
|
|
8108
8628
|
/** 剧情方案:基于健康检查结果设计下一阶段方向与建议新线。 */
|
|
8109
8629
|
async function designPlotlinePlan(ctx, config, project, health) {
|
|
8110
|
-
const system =
|
|
8111
|
-
"你是一位网文剧情架构师。请为本书设计「下一阶段的剧情方案」:给出未来 5-10 章的剧情方向,并建议 2-3 条值得新增的剧情线。",
|
|
8112
|
-
"要求:方向必须结合本书大纲/卷计划/现有线/编年录;新线要能落地(和当前主角处境、已有伏笔、下一阶段舞台相关),不得重复已有线。",
|
|
8113
|
-
"输出必须是合法 JSON 对象:{\"direction\": \"下一阶段方向 60-120 字\", \"suggestions\": [{\"name\": \"线名\", \"kind\": \"main|branch|character|mystery\", \"goal\": \"目标\", \"progress\": \"初始进度(可空)\"}]}",
|
|
8114
|
-
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。",
|
|
8115
|
-
"重要:直接输出 JSON 结果本身,不要把思考过程写在输出里。"
|
|
8116
|
-
].join("\n");
|
|
8630
|
+
const system = plotlinePlanSystemPrompt();
|
|
8117
8631
|
const written = project.chapters.filter((c) => c.status !== "pending" && c.status !== "generating");
|
|
8118
8632
|
const raw = parseJsonObject(await complete(ctx, config, {
|
|
8119
8633
|
system,
|
|
@@ -8157,10 +8671,6 @@ async function designPlotlinePlan(ctx, config, project, health) {
|
|
|
8157
8671
|
suggestions
|
|
8158
8672
|
};
|
|
8159
8673
|
}
|
|
8160
|
-
/** Build the rewrite system prompt (fix review issues / instructions). */
|
|
8161
|
-
function rewriteSystemPrompt(project, targetChars, beforeChapter) {
|
|
8162
|
-
return writeSystemPrompt(project, targetChars, "1. 输出完整的新正文(不要只输出修改片段、标题、章回名、作者的话或任何 Markdown 标记),字数与原章相当(允许 ±20%)。", beforeChapter) + "\n\n额外要求:你正在【修订】一章已写好的正文。保留原文中好的部分,只修改需要修改的地方,输出完整的新正文(不要只输出修改片段),字数与原文相当。";
|
|
8163
|
-
}
|
|
8164
8674
|
/**
|
|
8165
8675
|
* Stream a chapter rewrite. With `target` (a passage of the body), only that
|
|
8166
8676
|
* passage's paragraph is rewritten and spliced back — everything else stays
|
|
@@ -8205,29 +8715,7 @@ async function* rewriteChapterStream(ctx, config, project, outputDir, chapterNo,
|
|
|
8205
8715
|
"2. 保留该段的情节走向与角色口吻,只按修改要求调整。",
|
|
8206
8716
|
"3. 段落长度与原文相当。"
|
|
8207
8717
|
].filter((line) => line !== "").join("\n");
|
|
8208
|
-
const system = localTarget === void 0 ? rewriteSystemPrompt(project, chapter.targetChars || config.chapterChars, chapter.no) : (
|
|
8209
|
-
const bible = project.bible;
|
|
8210
|
-
const lines = [
|
|
8211
|
-
"你是一位中文网文润色师。你会收到一章中的一个段落,请按修改要求重写该段。",
|
|
8212
|
-
"硬性约束:",
|
|
8213
|
-
"1. 只改表达,不改情节走向、人物设定、已确立事实、对话核心内容。",
|
|
8214
|
-
"2. 必须遵守下方「内容合规红线」,任何一条命中(含影射、暗示)都必须避免。",
|
|
8215
|
-
"3. 必须遵守下方「本书红线」(如有)。",
|
|
8216
|
-
"4. 避免 AI 套话:不禁、仿佛、一时间、不由得、顿时、然而、缓缓、轻轻、微微、默默、似乎、终于等滥用。",
|
|
8217
|
-
"5. 保留角色口吻与性格,角色行为需符合下方角色卡(如有)。",
|
|
8218
|
-
"6. 只输出修改后的【这一个段落】的完整新文本,不要输出任何说明、标题或 Markdown 标记。"
|
|
8219
|
-
];
|
|
8220
|
-
if (bible !== void 0) {
|
|
8221
|
-
if (bible.redLines.length > 0) lines.push("本书红线:\n" + bible.redLines.map((r) => "- " + r).join("\n"));
|
|
8222
|
-
if (bible.characters.length > 0) {
|
|
8223
|
-
lines.push("相关角色卡:");
|
|
8224
|
-
for (const card of bible.characters) lines.push("- " + card.name + "(" + card.role + "):" + card.traits.join("、"));
|
|
8225
|
-
}
|
|
8226
|
-
}
|
|
8227
|
-
lines.push("内容合规红线(平台硬性要求,最高优先级):");
|
|
8228
|
-
lines.push(COMPLIANCE_REDLINES.join("\n"));
|
|
8229
|
-
return lines.join("\n");
|
|
8230
|
-
})();
|
|
8718
|
+
const system = localTarget === void 0 ? rewriteSystemPrompt(project, chapter.targetChars || config.chapterChars, chapter.no) : localRevisionSystemPrompt(project);
|
|
8231
8719
|
const messages = [createUserMessage({
|
|
8232
8720
|
content: [{
|
|
8233
8721
|
type: "text",
|
|
@@ -8276,26 +8764,6 @@ async function* rewriteChapterStream(ctx, config, project, outputDir, chapterNo,
|
|
|
8276
8764
|
draft: newBody
|
|
8277
8765
|
};
|
|
8278
8766
|
}
|
|
8279
|
-
/** The de-AI-ify polish system prompt (with project writing assets injected). */
|
|
8280
|
-
function polishSystemPrompt(project) {
|
|
8281
|
-
const assetsBlock = renderAllAssets(project.assets);
|
|
8282
|
-
const bible = project.bible;
|
|
8283
|
-
const lines = [
|
|
8284
|
-
"你是一位中文网文润色师。你会收到一章正文,请做「去 AI 味」润色:",
|
|
8285
|
-
"1. 删除/替换 AI 高频套话与模式词:如\"不禁\"\"仿佛\"\"一时间\"\"不由得\"\"顿时\"\"然而\"\"缓缓\"\"轻轻\"\"微微\"\"默默\"\"似乎\"\"终于\"等滥用。",
|
|
8286
|
-
"2. 把书面翻译腔改成口语化的中文网文语感。",
|
|
8287
|
-
"3. 拆分过长的排比句与堆砌的修饰语。",
|
|
8288
|
-
"4. 保留全部情节、人物、对话内容、已确立事实不变,只改表达。",
|
|
8289
|
-
"5. 输出完整的新正文,不要输出任何说明文字或 Markdown 标记。",
|
|
8290
|
-
"6. 必须遵守下方「反 AI 规则」与「写法资产」的表达边界;写法资产要求保留的风格特征(句式、台词、节奏)不得在润色中丢失。",
|
|
8291
|
-
"7. 必须遵守下方「内容合规红线」与「本书红线」(如有),任何一条命中(含影射、暗示)都必须避免。"
|
|
8292
|
-
];
|
|
8293
|
-
if (bible !== void 0 && bible.redLines.length > 0) lines.push("本书红线:\n" + bible.redLines.map((r) => "- " + r).join("\n"));
|
|
8294
|
-
lines.push("内容合规红线(平台硬性要求,最高优先级):");
|
|
8295
|
-
lines.push(COMPLIANCE_REDLINES.join("\n"));
|
|
8296
|
-
if (assetsBlock !== "") lines.push(assetsBlock);
|
|
8297
|
-
return lines.join("\n");
|
|
8298
|
-
}
|
|
8299
8767
|
/** Stream a chapter polish (de-AI-ify). Draft-mode: the polished body lands
|
|
8300
8768
|
* in `chapter.pendingDraft` and is only applied on draft/apply. */
|
|
8301
8769
|
async function* polishChapterStream(ctx, config, project, outputDir, chapterNo, signal) {
|
|
@@ -8452,7 +8920,7 @@ async function* generateChapterStream(ctx, config, project, outputDir, chapterNo
|
|
|
8452
8920
|
`请承接上文继续写约 ${need} 字,不要重复已有内容,不要重写开头,不要总结前文,直接续写新情节,结尾留一个章末钩子。只输出续写的正文,不要输出标题或任何说明。`
|
|
8453
8921
|
].join("\n");
|
|
8454
8922
|
const add = (await complete(ctx, config, {
|
|
8455
|
-
system: writeSystemPrompt(project, target, void 0, chapter.no) +
|
|
8923
|
+
system: writeSystemPrompt(project, target, void 0, chapter.no) + bodyContinuationSuffix(),
|
|
8456
8924
|
user: contUser,
|
|
8457
8925
|
temperature: .85,
|
|
8458
8926
|
maxTokens: Math.max(config.maxTokens, 12e3),
|
|
@@ -8488,11 +8956,7 @@ async function summarizeChapter(ctx, config, project, outputDir, chapterNo) {
|
|
|
8488
8956
|
const body = readChapterFile(outputDir, chapter);
|
|
8489
8957
|
if (body === void 0) throw new NovelActionError("contract", `章节 ${chapterNo} 的正文文件不存在`);
|
|
8490
8958
|
chapter.summary = (await complete(ctx, config, {
|
|
8491
|
-
system:
|
|
8492
|
-
"你是一位网文编辑。请为下面一章写一段 120-200 字的摘要,供后续章节写作时保持连贯性。",
|
|
8493
|
-
"摘要必须包含:本章发生的关键事件、主角状态变化(境界/资源/伤势/心境)、新增的伏笔或线索、角色关系变化。",
|
|
8494
|
-
"用客观陈述句,不要评价,不要剧透式感叹。只输出摘要正文。"
|
|
8495
|
-
].join("\n"),
|
|
8959
|
+
system: chapterSummarySystemPrompt(),
|
|
8496
8960
|
user: stripChapterHeadings(body),
|
|
8497
8961
|
temperature: .3,
|
|
8498
8962
|
maxTokens: Math.max(config.maxTokens, 4e3)
|
|
@@ -8512,12 +8976,13 @@ async function reverseOutlineFromChapters(ctx, config, project, outputDir, onPro
|
|
|
8512
8976
|
const notes = [];
|
|
8513
8977
|
const total = written.length;
|
|
8514
8978
|
for (let i = 0; i < written.length; i += BATCH) {
|
|
8979
|
+
const bodies = written.slice(i, i + BATCH).map((c) => {
|
|
8980
|
+
const body = readChapterFile(outputDir, c) ?? "";
|
|
8981
|
+
return "第" + c.no + "章《" + (c.title || "无题") + "》\n" + stripChapterHeadings(body).slice(0, 1e3);
|
|
8982
|
+
}).join("\n\n---\n\n");
|
|
8515
8983
|
const note = await complete(ctx, config, {
|
|
8516
|
-
system:
|
|
8517
|
-
user:
|
|
8518
|
-
const body = readChapterFile(outputDir, c) ?? "";
|
|
8519
|
-
return "第" + c.no + "章《" + (c.title || "无题") + "》\n" + stripChapterHeadings(body).slice(0, 1e3);
|
|
8520
|
-
}).join("\n\n---\n\n"),
|
|
8984
|
+
system: reverseOutlineBatchSystemPrompt(),
|
|
8985
|
+
user: bodies,
|
|
8521
8986
|
temperature: .2,
|
|
8522
8987
|
maxTokens: Math.max(config.maxTokens, 3e3),
|
|
8523
8988
|
reasoning: config.analysisReasoning ?? "low",
|
|
@@ -8528,14 +8993,7 @@ async function reverseOutlineFromChapters(ctx, config, project, outputDir, onPro
|
|
|
8528
8993
|
}
|
|
8529
8994
|
onProgress?.(total, total, "生成大纲");
|
|
8530
8995
|
const outline = await complete(ctx, config, {
|
|
8531
|
-
system:
|
|
8532
|
-
"你是一位经验丰富的小说主编。根据下面全书各章事件摘要,反推出这本书的总纲(大纲),可直接作为后续写作依据。要求:",
|
|
8533
|
-
"1. 第一行写《书名》(从摘要中的书名或内容推断,若无则用《未命名》)。",
|
|
8534
|
-
"2. 按故事弧线划分卷/部分:每卷给出卷名与主旨(标注覆盖章节范围)。",
|
|
8535
|
-
"3. 每一章列出:章节号 + 标题 + 一句话核心情节(若原章无标题可自拟)。",
|
|
8536
|
-
"4. 最后给出:全书主线、主要人物弧线、已埋设待回收的伏笔清单。",
|
|
8537
|
-
"5. 输出为纯文本 Markdown 结构(# 一级标题、## 二级标题、- 列表),不要多余寒暄。"
|
|
8538
|
-
].join("\n"),
|
|
8996
|
+
system: reverseOutlineSystemPrompt(),
|
|
8539
8997
|
user: notes.join("\n\n"),
|
|
8540
8998
|
temperature: .4,
|
|
8541
8999
|
maxTokens: Math.max(config.maxTokens, 6e3),
|
|
@@ -8566,16 +9024,8 @@ async function analyzeAdaptation(ctx, config, text) {
|
|
|
8566
9024
|
}
|
|
8567
9025
|
if (n > 1) pick(n - 1);
|
|
8568
9026
|
const raw = parseJsonObject(await complete(ctx, config, {
|
|
8569
|
-
system:
|
|
8570
|
-
user:
|
|
8571
|
-
"你是一位资深网文编辑兼改编策划。下面给你一部已完结/连载小说的若干章正文节选。请通读并输出该书的「原文设定卡片」与「可改范围矩阵」。",
|
|
8572
|
-
"输出合法 JSON 对象:",
|
|
8573
|
-
"{\"bookName\": \"书名\", \"outline\": \"一句话主线梗概(100字内)\", \"dimensions\": [{\"key\":\"realm\",\"title\":\"大世界\",\"mutability\":\"big|small|free|locked|visual\",\"current\":\"当前值\",\"evidence\":\"证据(出现章节/频次)\",\"candidates\":[{\"name\":\"候选体系名\",\"desc\":\"该体系一句话说明\"}],\"impact\":\"改了会影响什么\",\"risk\":\"high|medium|low\"}]}",
|
|
8574
|
-
"dimensions 至少覆盖以下维度(key/title):realm 大世界、cultivation 修为体系、protagonist 主角、goldenFinger 金手指、supporting 配角与势力人物名、faction 势力/组织、style 文风与叙事、ending 结局走向、timeline 时间线/编年、foreshadow 伏笔/暗线。",
|
|
8575
|
-
"mutability 取值:locked=建议保留、big=可改影响大、small=可改影响小、free=可自由改、visual=仅视觉包装。",
|
|
8576
|
-
"每个 dimension.current 必须忠于文本,能引用原文就用原文(尤其角色名/境界名/势力名/金手指名)。",
|
|
8577
|
-
"重要:所有字符串值内部不得包含换行符;JSON 必须在一段内完整结束;直接输出 JSON,不要 Markdown 代码块。"
|
|
8578
|
-
].join("\n") + "\n\n" + sampleBodies.join("\n\n---\n\n"),
|
|
9027
|
+
system: adaptationAnalyzeSystemPrompt(),
|
|
9028
|
+
user: adaptationAnalyzeUser(sampleBodies),
|
|
8579
9029
|
temperature: .3,
|
|
8580
9030
|
maxTokens: Math.max(config.maxTokens, 6e3),
|
|
8581
9031
|
reasoning: config.analysisReasoning ?? "low"
|
|
@@ -8648,15 +9098,7 @@ async function reverseOutlineFromAdaptationText(ctx, config, chapters) {
|
|
|
8648
9098
|
sample.push("第" + c.no + "章《" + (c.title || "无题") + "》\n" + c.body.slice(0, 600));
|
|
8649
9099
|
}
|
|
8650
9100
|
return (await complete(ctx, config, {
|
|
8651
|
-
system:
|
|
8652
|
-
"你是一位经验丰富的小说主编。根据下面若干章节的正文节选,反推出这本书的总纲(可作为后续改编与章节续写的骨架)。",
|
|
8653
|
-
"要求:",
|
|
8654
|
-
"1. 第一行写《书名》(可从正文推断,否则《未命名》)。",
|
|
8655
|
-
"2. 按故事弧线划分卷/部分:每卷给卷名与主旨(标注覆盖章节范围)。",
|
|
8656
|
-
"3. 对每章列出:章节号 + 标题 + 一句话核心情节。",
|
|
8657
|
-
"4. 最后给出:全书主线、主要人物弧线、已埋设待回收的伏笔清单。",
|
|
8658
|
-
"5. 输出纯文本 Markdown 结构(# 一级标题、## 二级标题、- 列表),不要寒暄,不要其他输出。"
|
|
8659
|
-
].join("\n"),
|
|
9101
|
+
system: reverseOutlineFromAdaptationSystemPrompt(),
|
|
8660
9102
|
user: sample.join("\n\n---\n\n"),
|
|
8661
9103
|
temperature: .4,
|
|
8662
9104
|
maxTokens: Math.max(config.maxTokens, 6e3),
|
|
@@ -8665,23 +9107,11 @@ async function reverseOutlineFromAdaptationText(ctx, config, chapters) {
|
|
|
8665
9107
|
}
|
|
8666
9108
|
/** 改编方案:由用户勾选的维度与新值,生成 LLM 映射表/规则/影响清单。 */
|
|
8667
9109
|
async function proposeAdaptation(ctx, config, _text, selections, dimensions) {
|
|
9110
|
+
const selLines = selections.map((s) => "- " + s.title + "(" + s.key + "):" + s.current + " → " + s.target).join("\n");
|
|
9111
|
+
const dimLines = (dimensions ?? []).map((d) => "- " + d.title + ":" + d.current + "(可改度:" + d.mutability + ",风险:" + d.risk + ")").join("\n");
|
|
8668
9112
|
const raw = parseJsonObject(await complete(ctx, config, {
|
|
8669
|
-
system:
|
|
8670
|
-
user:
|
|
8671
|
-
"下面给出改编决策:用户想改哪些维度、改成什么值。请生成一份可执行的「改编方案」。",
|
|
8672
|
-
"要求输出合法 JSON 对象:",
|
|
8673
|
-
"{\"mappings\": [{\"source\":\"原值\",\"target\":\"新值\",\"scope\":\"name|realm|faction|term|other\",\"note\":\"说明\"}], \"rules\": {\"preserve\":[\"必须保留的要素\"],\"change\":[\"允许改变的要素\"],\"constraints\":[\"改编红线/一致性要求\"]}, \"impacts\": [{\"item\":\"受影响项\",\"detail\":\"说明\",\"risk\":\"high|medium|low\",\"chapters\":[章号]}]}",
|
|
8674
|
-
"mappings 需把用户确认的新值展开为「原→新」条目(如主角名/境界名/势力名/术语),并补充用户未填但关联的必改项(如改了修为体系名,相关的境界名一并列映射)。",
|
|
8675
|
-
"rules.preserve 至少包含:故事骨架、人物动机、伏笔逻辑、爽点结构。",
|
|
8676
|
-
"impacts 列出每个改动会影响的内容(术语/角色/章节/伏笔),能定位章号尽量定位;无法定位则给章节区间提示。",
|
|
8677
|
-
"重要:所有字符串值内部不得包含换行符;JSON 必须在一段内完整结束;直接输出 JSON,不要 Markdown 代码块。",
|
|
8678
|
-
"",
|
|
8679
|
-
"用户要改:",
|
|
8680
|
-
selections.map((s) => "- " + s.title + "(" + s.key + "):" + s.current + " → " + s.target).join("\n"),
|
|
8681
|
-
"",
|
|
8682
|
-
"原文可改矩阵(已分析):",
|
|
8683
|
-
(dimensions ?? []).map((d) => "- " + d.title + ":" + d.current + "(可改度:" + d.mutability + ",风险:" + d.risk + ")").join("\n")
|
|
8684
|
-
].join("\n"),
|
|
9113
|
+
system: adaptationProposeSystemPrompt(),
|
|
9114
|
+
user: adaptationProposeUser(selLines, dimLines),
|
|
8685
9115
|
temperature: .3,
|
|
8686
9116
|
maxTokens: Math.max(config.maxTokens, 6e3),
|
|
8687
9117
|
reasoning: config.analysisReasoning ?? "low"
|
|
@@ -8781,16 +9211,7 @@ async function rewriteAdaptationBook(ctx, config, text, mappings, rules, options
|
|
|
8781
9211
|
rules.change.length > 0 ? "允许改变:\n" + rules.change.map((x) => `- ${x}`).join("\n") : "",
|
|
8782
9212
|
rules.constraints.length > 0 ? "改编红线/一致性要求:\n" + rules.constraints.map((x) => `- ${x}`).join("\n") : ""
|
|
8783
9213
|
].filter((s) => s !== "").join("\n") : "";
|
|
8784
|
-
const system =
|
|
8785
|
-
"你是一位资深网文改编编剧。你会收到某一章的正文,以及本书的改编映射表与改编规则。",
|
|
8786
|
-
"任务:把这一章按改编方案**重写**成新版本(结构性改编,不只是换词)。",
|
|
8787
|
-
"要求:",
|
|
8788
|
-
"1. 严格遵守映射表(原值→新值),正文中所有源值都要替换为新值;涉及改名/改体系/改势力时,相关表述一起调整,使上下文自洽。",
|
|
8789
|
-
"2. 改编规则:必须保留的内容不得破坏(故事骨架/人物动机/伏笔逻辑/爽点结构);允许改变的内容可以放开调整;红线/一致性要求必须遵守。",
|
|
8790
|
-
"3. 若某个改动牵动叙事(如结局走向/时间线/世界观),要把这章的叙述顺势改得通顺、可信。",
|
|
8791
|
-
"4. 输出**只包含这一章重写后的正文**,不要重复标题,不要任何解释、开头或结尾。",
|
|
8792
|
-
"5. 字数与原章基本相当(允许 ±20%)。"
|
|
8793
|
-
].join("\n");
|
|
9214
|
+
const system = adaptationRewriteSystemPrompt();
|
|
8794
9215
|
const adaptedParts = [];
|
|
8795
9216
|
const rewritten = [];
|
|
8796
9217
|
const skipped = [];
|
|
@@ -9038,12 +9459,7 @@ async function summarizeAndExtractFacts(ctx, config, project, outputDir, chapter
|
|
|
9038
9459
|
factCount: 0
|
|
9039
9460
|
};
|
|
9040
9461
|
const raw = parseJsonObject(await complete(ctx, config, {
|
|
9041
|
-
system:
|
|
9042
|
-
"你是一位网文编辑。请为下面一章做两件事,输出合法 JSON 对象:",
|
|
9043
|
-
"{\"summary\": \"120-200字摘要,含关键事件/主角状态变化(境界资源伤势心境)/新增伏笔线索/角色关系变化,客观陈述不评价\", \"facts\": [\"已确立事实1\", \"…3-6条\"]}",
|
|
9044
|
-
"facts 指:本章明确写出的、对后续有约束力的事实——人物当前状态、重要关系变化、地点与时间线、已落地或新增的伏笔线索、关键道具去向。",
|
|
9045
|
-
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。"
|
|
9046
|
-
].join("\n"),
|
|
9462
|
+
system: summaryFactsSystemPrompt(),
|
|
9047
9463
|
user: stripChapterHeadings(body),
|
|
9048
9464
|
temperature: .2,
|
|
9049
9465
|
maxTokens: Math.max(config.maxTokens, 5e3)
|
|
@@ -9093,20 +9509,7 @@ async function summarizeFactsAndTimeline(ctx, config, project, outputDir, chapte
|
|
|
9093
9509
|
const wantTimeline = options.timeline !== false;
|
|
9094
9510
|
const NL = String.fromCharCode(10);
|
|
9095
9511
|
const raw = parseJsonObject(await complete(ctx, config, {
|
|
9096
|
-
system:
|
|
9097
|
-
"你是一位网文编辑。请为下面一章做三件事,输出**一个**合法 JSON 对象:",
|
|
9098
|
-
"{\"summary\": \"120-200字摘要,含关键事件/主角状态变化(境界资源伤势心境)/新增伏笔线索/角色关系变化,客观陈述不评价\",",
|
|
9099
|
-
" \"facts\": [\"已确立事实1\", \"…3-6条\"],",
|
|
9100
|
-
...wantTimeline ? [" \"timeline\": [{\"time\":\"第三日黄昏\",\"order\":1,\"place\":\"青云宗外门\",\"characters\":[\"沈青\"],\"event\":\"不超过40字\"}]"] : [],
|
|
9101
|
-
"",
|
|
9102
|
-
"facts 指:本章明确写出的、对后续有约束力的事实——人物当前状态、重要关系变化、地点与时间线、已落地或新增的伏笔线索、关键道具去向。",
|
|
9103
|
-
...wantTimeline ? [
|
|
9104
|
-
"timeline 指:只抽取正文里真实发生的场景节点(3-8 条),不要抽预告、回忆性泛泛陈述;纯粹的回忆/插叙请在 event 里注明「(回忆)」并给出其故事内时间。",
|
|
9105
|
-
"time 用正文里的说法(如「第三日黄昏」「入宗三个月后」「同日夜」);正文没写时间就填相对说法(如「紧接上一场景」)。",
|
|
9106
|
-
"order 是本章内的先后序号,从 1 递增;place 是一处一个词;characters 只列在场的角色名(用正文里的名字,不要写「主角」)。"
|
|
9107
|
-
] : [],
|
|
9108
|
-
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束,不要输出 Markdown 代码块标记。"
|
|
9109
|
-
].join(NL),
|
|
9512
|
+
system: summaryFactsTimelineSystemPrompt(wantTimeline),
|
|
9110
9513
|
user: `第 ${chapterNo} 章《${chapter.title}》正文:${NL}${NL}${stripChapterHeadings(body).slice(0, 24e3)}`,
|
|
9111
9514
|
temperature: .2,
|
|
9112
9515
|
maxTokens: options.maxTokens ?? Math.max(config.maxTokens, wantTimeline ? 8e3 : 5e3),
|
|
@@ -9172,14 +9575,7 @@ async function extractFacts(ctx, config, project, outputDir, chapterNo) {
|
|
|
9172
9575
|
const body = readChapterFile(outputDir, chapter);
|
|
9173
9576
|
if (body === void 0) return 0;
|
|
9174
9577
|
const lines = (await complete(ctx, config, {
|
|
9175
|
-
system:
|
|
9176
|
-
"你是一位网文编辑。请从本章正文中抽取「已确立事实」,供后续章节保持一致。",
|
|
9177
|
-
"事实指:人物当前状态(境界/修为/伤势/资源/心境)、重要关系变化、地点与时间线、已落地或新增的伏笔线索、关键道具去向。",
|
|
9178
|
-
"要求:",
|
|
9179
|
-
"1. 只抽取本章明确写出的、对后续有约束力的内容;纯心理活动与无关细节不要。",
|
|
9180
|
-
"2. 每行一条事实,用客观陈述句,不含主观评价。",
|
|
9181
|
-
"3. 输出 3-6 条,每行一条,不要编号、不要前缀、不要解释。"
|
|
9182
|
-
].join("\n"),
|
|
9578
|
+
system: extractFactsSystemPrompt(),
|
|
9183
9579
|
user: stripChapterHeadings(body),
|
|
9184
9580
|
temperature: .2,
|
|
9185
9581
|
maxTokens: Math.max(config.maxTokens, 4e3)
|
|
@@ -9198,19 +9594,7 @@ async function extractFacts(ctx, config, project, outputDir, chapterNo) {
|
|
|
9198
9594
|
const AUDIT_BATCH_SIZE = 10;
|
|
9199
9595
|
/** 单批质检:设定 + 事实库 + 该批章节节选 → 矛盾清单。 */
|
|
9200
9596
|
async function auditBatch(ctx, config, project, outputDir, batch) {
|
|
9201
|
-
const system =
|
|
9202
|
-
"你是一位严谨的网文连续性审校编辑。你会收到一本小说的道藏、事实库和一批章节正文节选。",
|
|
9203
|
-
"请找出这批章节中的一致性矛盾,例如:",
|
|
9204
|
-
"- 人物状态冲突:境界/修为/伤势/资源在同一章内或跨章前后矛盾。",
|
|
9205
|
-
"- 设定违背:正文与世界观规则、金手指规则、写作红线冲突。",
|
|
9206
|
-
"- 时间线错乱:事件顺序、时间跨度、地点移动不合逻辑。",
|
|
9207
|
-
"- 细节穿帮:人名/地名/物品/数字前后不一致。",
|
|
9208
|
-
"要求:",
|
|
9209
|
-
"1. 只报告有实质证据的矛盾,不要泛泛而谈写作质量问题。",
|
|
9210
|
-
"2. 每条必须定位到具体章节号。",
|
|
9211
|
-
"3. 输出必须是合法 JSON 数组,格式:[{\"chapterNo\": 章节号, \"severity\": \"high|medium|low\", \"item\": \"矛盾描述\", \"suggestion\": \"修改建议\"}]",
|
|
9212
|
-
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。"
|
|
9213
|
-
].join("\n");
|
|
9597
|
+
const system = auditBatchSystemPrompt();
|
|
9214
9598
|
const factsBlock = (project.facts ?? []).slice(-60).map((f) => `[第${f.chapterNo}章] ${f.text}`).join("\n");
|
|
9215
9599
|
const chapterBlocks = batch.map((c) => {
|
|
9216
9600
|
const excerpt = stripChapterHeadings(readChapterFile(outputDir, c) ?? "");
|
|
@@ -9273,13 +9657,7 @@ async function auditBook(ctx, config, project, outputDir, onProgress) {
|
|
|
9273
9657
|
}
|
|
9274
9658
|
/** 小说简介:生成或按已写开头补全(面向读者的作品门面)。 */
|
|
9275
9659
|
async function generateBlurb(ctx, config, project, partial = "") {
|
|
9276
|
-
const system =
|
|
9277
|
-
"你是一位网文平台编辑,擅长写抓人的作品简介。",
|
|
9278
|
-
"要求:",
|
|
9279
|
-
"1. 120-250 字,突出核心卖点(金手指/题材/爽点/人设反差),用一两句抛出开局钩子。",
|
|
9280
|
-
"2. 不剧透结局与关键反转;语气贴合题材(热血/悬疑/轻松/虐心)。",
|
|
9281
|
-
"3. 中文,直接输出简介正文,不要 Markdown、不要引号包裹、不要「简介:」前缀。"
|
|
9282
|
-
].join("\n");
|
|
9660
|
+
const system = blurbSystemPrompt();
|
|
9283
9661
|
const genreBlock = project.bible?.genre !== void 0 ? `题材:${project.bible.genre}` : "";
|
|
9284
9662
|
const volumeBlock = (project.volumes ?? []).slice(0, 3).map((v) => v.title).join("、");
|
|
9285
9663
|
return (await complete(ctx, config, {
|
|
@@ -9369,12 +9747,7 @@ function bookOverview(project, scope = "recent") {
|
|
|
9369
9747
|
* 定位一次改动波及的所有位置。助手在修改后主动调用,做连锁维护。
|
|
9370
9748
|
*/
|
|
9371
9749
|
async function analyzeImpact(ctx, config, project, outputDir, change) {
|
|
9372
|
-
const system =
|
|
9373
|
-
"你是一位网文一致性审校。作者要做一处修改,请找出这次改动会波及的所有位置(设定、大纲、已写章节正文、事实库、简介中可能因此过时或矛盾的内容)。",
|
|
9374
|
-
"输出必须是合法 JSON 数组,格式:[{\"location\": \"位置(第N章/大纲/道藏-世界规则/大世界-境界/事实库/简介)\", \"quote\": \"原文片段(20-60字)\", \"suggestion\": \"修改建议\", \"kind\": \"must|optional|note\"}]",
|
|
9375
|
-
"kind 含义:must=必须同步改否则矛盾;optional=建议改(影响观感);note=备注(如旧称保留为古称、或无需改但需知晓)。",
|
|
9376
|
-
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。"
|
|
9377
|
-
].join("\n");
|
|
9750
|
+
const system = impactAnalysisSystemPrompt();
|
|
9378
9751
|
const written = project.chapters.filter((c) => c.status !== "pending" && c.status !== "generating");
|
|
9379
9752
|
const base = [
|
|
9380
9753
|
`要做的修改:${change}`,
|
|
@@ -9413,38 +9786,9 @@ async function analyzeImpact(ctx, config, project, outputDir, change) {
|
|
|
9413
9786
|
}
|
|
9414
9787
|
return items.slice(0, 30);
|
|
9415
9788
|
}
|
|
9416
|
-
/** 把大世界结构化数据渲染成提示词块(境界体系按顺序强约束)。 */
|
|
9417
|
-
function renderWorld(world) {
|
|
9418
|
-
if (world === void 0) return "";
|
|
9419
|
-
const sections = ["==================== 大世界(结构化设定,写作时严格遵守) ===================="];
|
|
9420
|
-
if (world.realms.length > 0) {
|
|
9421
|
-
sections.push("境界体系(由低到高,不得随意跳级或自创境界):");
|
|
9422
|
-
world.realms.forEach((realm, i) => {
|
|
9423
|
-
sections.push(`${i + 1}. ${realm.name}${realm.description !== "" ? ` — ${realm.description}` : ""}`);
|
|
9424
|
-
});
|
|
9425
|
-
}
|
|
9426
|
-
if (world.regions.length > 0) {
|
|
9427
|
-
sections.push("地理区域:");
|
|
9428
|
-
for (const region of world.regions) sections.push(`- ${region.name}${region.description !== "" ? `:${region.description}` : ""}${region.faction !== void 0 && region.faction !== "" ? `(势力:${region.faction})` : ""}`);
|
|
9429
|
-
}
|
|
9430
|
-
if (world.factions.length > 0) {
|
|
9431
|
-
sections.push("势力分布:");
|
|
9432
|
-
for (const faction of world.factions) sections.push(`- ${faction.name}(${faction.kind})${faction.description !== "" ? `:${faction.description}` : ""}${faction.region !== void 0 && faction.region !== "" ? `(驻地:${faction.region})` : ""}`);
|
|
9433
|
-
}
|
|
9434
|
-
return sections.join("\n");
|
|
9435
|
-
}
|
|
9436
9789
|
/** 提炼大世界:从大纲 + 道藏生成结构化境界体系/区域/势力。 */
|
|
9437
9790
|
async function extractWorld(ctx, config, project) {
|
|
9438
|
-
const system =
|
|
9439
|
-
"你是一位网文世界观架构师。请根据小说大纲与道藏,提炼结构化「大世界」数据。",
|
|
9440
|
-
"输出必须是合法 JSON 对象:",
|
|
9441
|
-
"{\"realms\": [{\"name\": \"境界名\", \"description\": \"突破条件/寿命/标志等\"}], \"regions\": [{\"name\": \"区域名\", \"description\": \"描述\", \"faction\": \"关联势力名或空\"}], \"factions\": [{\"name\": \"势力名\", \"kind\": \"宗门/家族/王朝/组织等\", \"description\": \"描述\", \"region\": \"驻地区域或空\"}]}",
|
|
9442
|
-
"要求:",
|
|
9443
|
-
"1. realms 按由低到高顺序排列(修仙题材必须含完整境界链;无境界设定的题材可输出空数组)。",
|
|
9444
|
-
"2. 数量贴合大纲:realms 3-12 个,regions 2-10 个,factions 2-10 个。",
|
|
9445
|
-
"3. 内容严格来自大纲与道藏,不要凭空发明与大纲冲突的设定。",
|
|
9446
|
-
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。"
|
|
9447
|
-
].join("\n");
|
|
9791
|
+
const system = extractWorldSystemPrompt();
|
|
9448
9792
|
const bibleBlock = project.bible !== void 0 ? [project.bible.genre !== "" ? `题材:${project.bible.genre}` : "", project.bible.worldRules.length > 0 ? `世界规则:\n${project.bible.worldRules.map((r) => `- ${r}`).join("\n")}` : ""].filter((s) => s !== "").join("\n") : "";
|
|
9449
9793
|
const raw = parseJsonObject(await complete(ctx, config, {
|
|
9450
9794
|
system,
|
|
@@ -9526,11 +9870,7 @@ async function refreshCharacters(ctx, config, project, outputDir) {
|
|
|
9526
9870
|
}
|
|
9527
9871
|
let statuses = /* @__PURE__ */ new Map();
|
|
9528
9872
|
if (facts.length > 0) {
|
|
9529
|
-
const system =
|
|
9530
|
-
"你是一位网文角色档案管理员。请根据「角色名单」与「已确立事实库」,为每个角色输出「当前状态」一句话(境界/修为/伤势/资源/心境)。",
|
|
9531
|
-
"输出必须是合法 JSON 数组,格式:[{\"name\": \"角色名\", \"status\": \"当前状态一句话\"}]",
|
|
9532
|
-
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。"
|
|
9533
|
-
].join("\n");
|
|
9873
|
+
const system = roleStatusSystemPrompt();
|
|
9534
9874
|
const rosterBlock = roster.map((ch) => `- ${ch.name}(${ch.traits.join("、")})`).join("\n");
|
|
9535
9875
|
const factsBlock = facts.map((f) => `[第${f.chapterNo}章] ${f.text}`).join("\n");
|
|
9536
9876
|
const user = [
|
|
@@ -9572,19 +9912,6 @@ async function refreshCharacters(ctx, config, project, outputDir) {
|
|
|
9572
9912
|
});
|
|
9573
9913
|
return cards;
|
|
9574
9914
|
}
|
|
9575
|
-
/** System prompt for foreshadow suggestions. */
|
|
9576
|
-
function foreshadowSystemPrompt() {
|
|
9577
|
-
return [
|
|
9578
|
-
"你是一位网文伏笔设计师。你会收到大纲和已写的章节信息,请为小说建议 3-8 条值得埋设的伏笔。",
|
|
9579
|
-
"要求:",
|
|
9580
|
-
"1. 伏笔必须有明确的回收价值(推动主线、人物弧光、世界观揭秘)。",
|
|
9581
|
-
"2. 描述要具体,指出埋设章节与预计回收章节(可空缺)。",
|
|
9582
|
-
"3. 优先从大纲的暗线(如记忆代价、残片收集、身世谜团)中提炼。",
|
|
9583
|
-
"输出必须是合法 JSON 数组:",
|
|
9584
|
-
"[{\"description\": \"伏笔描述\", \"plantedChapter\": 章节号或null, \"targetChapter\": 章节号或null}]",
|
|
9585
|
-
"重要:所有字符串值内部不得包含换行符,JSON 必须在一段内完整结束。"
|
|
9586
|
-
].join("\n");
|
|
9587
|
-
}
|
|
9588
9915
|
/** Suggest foreshadows from the outline + plan. */
|
|
9589
9916
|
async function suggestForeshadows(ctx, config, project) {
|
|
9590
9917
|
const user = [
|
|
@@ -9674,18 +10001,6 @@ async function extractStyleFormula(ctx, config, sampleText, depth) {
|
|
|
9674
10001
|
applyGuidance: typeof raw.applyGuidance === "string" ? raw.applyGuidance.trim().slice(0, 300) : ""
|
|
9675
10002
|
};
|
|
9676
10003
|
}
|
|
9677
|
-
/** 市场雷达系统提示(对齐上游 marketRadar 分析师角色)。 */
|
|
9678
|
-
function marketRadarSystemPrompt() {
|
|
9679
|
-
return [
|
|
9680
|
-
"你是中文网络文学市场分析师。只分析输入中的公开榜单元数据或用户给出的市场线索,不补写作品正文,不假装知道未提供的信息。",
|
|
9681
|
-
"只根据输入归纳热门题材和市场信号;对未提供的信息不要臆测。",
|
|
9682
|
-
"重点分析:热门题材组合、主角身份、金手指机制、开篇危机、关系卖点、标题句式、拥挤套路和差异化机会。",
|
|
9683
|
-
"kind 只能使用 genre、protagonist、advantage、opening、relationship、title_pattern、opportunity、crowding,不得创造近义枚举值。",
|
|
9684
|
-
"榜单高频不等于适合照搬。机会建议必须说明读者满足点,同时避开直接复制具体作品。",
|
|
9685
|
-
"productionFoundation 的题材/推进模式优先引用「资源库」中已有的 existingId(名称一字不差);只有确无合适资产时才给出新资产(existingId 为 null)。",
|
|
9686
|
-
"creativeBrief 严禁复用榜单作品的人名、专有设定、简介句子和完整书名;只能提炼读者需求、爽点机制和结构机会,不得输出任何具体人名/作品名,一律使用身份或通用场景称谓。"
|
|
9687
|
-
].join("\n");
|
|
9688
|
-
}
|
|
9689
10004
|
/** 题材雷达:输入平台/题材/榜单文本 → 信号 + 生产底座 + 开书创意。 */
|
|
9690
10005
|
async function runMarketRadar(ctx, config, req) {
|
|
9691
10006
|
const genreCatalog = BUILTIN_GENRE_LIBRARY.map((g, i) => `- [${i}] ${g.name}:${g.description}${g.template !== void 0 ? `(写法:${g.template})` : ""}`).join("\n");
|
|
@@ -9798,7 +10113,7 @@ async function runMarketCreativeBrief(ctx, config, req) {
|
|
|
9798
10113
|
/** 书分析/拆书:输入一本书/章节文本 → 卖点/结构/可借鉴点/风险。 */
|
|
9799
10114
|
async function runBookAnalysis(ctx, config, req) {
|
|
9800
10115
|
const raw = parseJsonObject(await complete(ctx, config, {
|
|
9801
|
-
system:
|
|
10116
|
+
system: bookAnalysisSystemPrompt(),
|
|
9802
10117
|
user: [
|
|
9803
10118
|
"请分析下面文本:\n" + (req.text ?? "").slice(0, 8e3),
|
|
9804
10119
|
"输出必须是合法 JSON 对象。",
|
|
@@ -9820,7 +10135,7 @@ async function runBookAnalysis(ctx, config, req) {
|
|
|
9820
10135
|
async function runIdeaInspiration(ctx, config, req) {
|
|
9821
10136
|
const count = Math.max(1, Math.min(10, req.count ?? 5));
|
|
9822
10137
|
const raw = parseJsonObject(await complete(ctx, config, {
|
|
9823
|
-
system:
|
|
10138
|
+
system: ideaInspirationSystemPrompt(),
|
|
9824
10139
|
user: [
|
|
9825
10140
|
`我的方向:${req.idea ?? ""}`,
|
|
9826
10141
|
`请给 ${count} 个开书创意。`,
|
|
@@ -9844,7 +10159,7 @@ async function runIdeaInspiration(ctx, config, req) {
|
|
|
9844
10159
|
/** 雷达→灵感:基于市场信号/生产底座/创意简报,生成贴合市场的开书灵感。 */
|
|
9845
10160
|
async function runMarketIdeaInspiration(ctx, config, req) {
|
|
9846
10161
|
const count = Math.max(1, Math.min(10, req.count ?? 5));
|
|
9847
|
-
const system =
|
|
10162
|
+
const system = marketIdeaInspirationSystemPrompt();
|
|
9848
10163
|
const signalBlock = (req.signals ?? []).map((s) => `- [${s.kind}] ${s.title}:${s.detail}`).join("\n") || "(无)";
|
|
9849
10164
|
const f = req.foundation;
|
|
9850
10165
|
const foundationBlock = f !== void 0 ? `题材:${f.genre.name}(${f.genre.description})\n主推进:${f.primaryStoryMode.name}(${f.primaryStoryMode.driver})${f.secondaryStoryMode !== void 0 ? `\n副推进:${f.secondaryStoryMode.name}` : ""}` : "(无)";
|
|
@@ -9883,22 +10198,23 @@ async function runDirectorAdvice(ctx, config, project, req) {
|
|
|
9883
10198
|
const plotlinesBlock = (project.plotlines ?? []).filter((l) => l.status === "active" || l.status === "paused").map((l) => `[${l.kind}] ${l.name}:${l.goal}${l.progress ? `(${l.progress})` : ""}`).join("\n") || "无";
|
|
9884
10199
|
const foreBlock = (project.foreshadows ?? []).filter((f) => f.status === "planted" || f.status === "progressing").map((f) => `- ${f.description}${f.targetChapter ? `(约${f.targetChapter}章回收)` : ""}`).join("\n") || "无";
|
|
9885
10200
|
const recentFacts = (project.facts ?? []).slice(-8).map((f) => `[第${f.chapterNo}章] ${f.text}`).join("\n");
|
|
10201
|
+
const user = [
|
|
10202
|
+
`书名:《${project.bookName}》`,
|
|
10203
|
+
req.focus !== void 0 && req.focus !== "" ? `聚焦:${req.focus}` : "",
|
|
10204
|
+
`当前进度:已写 ${done.length} 章,最后一章《${last?.title ?? "无"}》摘要:${last?.summary ?? ""}`,
|
|
10205
|
+
"",
|
|
10206
|
+
`分卷:\n${volumesBlock}`,
|
|
10207
|
+
`剧情线:\n${plotlinesBlock}`,
|
|
10208
|
+
`活跃伏笔:\n${foreBlock}`,
|
|
10209
|
+
`最近事实:\n${recentFacts}`,
|
|
10210
|
+
"",
|
|
10211
|
+
"请作为本书的自动导演,给出下一卷/下一阶段的编排建议:续写方向、阶段弧光、节奏板、风险提示、需要修复/再平衡的点。",
|
|
10212
|
+
"输出必须是合法 JSON 对象。",
|
|
10213
|
+
"JSON 结构:{\"summary\":\"总体判断一句话\",\"nextArc\":[\"下一阶段关键剧情节点(每条约一句话)\"],\"pacing\":\"节奏板(起承转合/爽点密度/章节节奏)\",\"risks\":[\"风险/跑偏提示\"],\"fixes\":[\"需要修复/再平衡的点\"]}"
|
|
10214
|
+
].filter((s) => s !== "").join("\n\n");
|
|
9886
10215
|
const raw = parseJsonObject(await complete(ctx, config, {
|
|
9887
|
-
system:
|
|
9888
|
-
user
|
|
9889
|
-
`书名:《${project.bookName}》`,
|
|
9890
|
-
req.focus !== void 0 && req.focus !== "" ? `聚焦:${req.focus}` : "",
|
|
9891
|
-
`当前进度:已写 ${done.length} 章,最后一章《${last?.title ?? "无"}》摘要:${last?.summary ?? ""}`,
|
|
9892
|
-
"",
|
|
9893
|
-
`分卷:\n${volumesBlock}`,
|
|
9894
|
-
`剧情线:\n${plotlinesBlock}`,
|
|
9895
|
-
`活跃伏笔:\n${foreBlock}`,
|
|
9896
|
-
`最近事实:\n${recentFacts}`,
|
|
9897
|
-
"",
|
|
9898
|
-
"请作为本书的自动导演,给出下一卷/下一阶段的编排建议:续写方向、阶段弧光、节奏板、风险提示、需要修复/再平衡的点。",
|
|
9899
|
-
"输出必须是合法 JSON 对象。",
|
|
9900
|
-
"JSON 结构:{\"summary\":\"总体判断一句话\",\"nextArc\":[\"下一阶段关键剧情节点(每条约一句话)\"],\"pacing\":\"节奏板(起承转合/爽点密度/章节节奏)\",\"risks\":[\"风险/跑偏提示\"],\"fixes\":[\"需要修复/再平衡的点\"]}"
|
|
9901
|
-
].filter((s) => s !== "").join("\n\n"),
|
|
10216
|
+
system: directorSystemPrompt(),
|
|
10217
|
+
user,
|
|
9902
10218
|
temperature: .4,
|
|
9903
10219
|
maxTokens: Math.max(config.maxTokens, 4e3),
|
|
9904
10220
|
liveLabel: "自动导演"
|
|
@@ -9932,18 +10248,7 @@ async function extractTimelineForChapter(ctx, config, project, outputDir, chapte
|
|
|
9932
10248
|
if (body === void 0) throw new NovelActionError("contract", `章节 ${chapterNo} 的正文文件不存在`);
|
|
9933
10249
|
const NL = String.fromCharCode(10);
|
|
9934
10250
|
const text = await complete(ctx, config, {
|
|
9935
|
-
system:
|
|
9936
|
-
"你是小说连续性编辑。任务:把一章正文压缩成「故事时间线」事件,供后续章节保持时间与地点的连续性。",
|
|
9937
|
-
"要求:",
|
|
9938
|
-
"1. 只抽取**正文里真实发生**的场景节点(3-8 条),不要抽取预告、回忆性的泛泛陈述;纯粹的回忆/插叙请在 event 里注明「(回忆)」并给出其故事内时间。",
|
|
9939
|
-
"2. time 用正文里的说法(如「第三日黄昏」「入宗三个月后」「同日夜」);正文没写时间就填相对说法(如「紧接上一场景」)。",
|
|
9940
|
-
"3. order 是本章内的先后序号,从 1 递增;跨章我会用章号排序,你只需要保证章内顺序正确。",
|
|
9941
|
-
"4. place 是场景地点(一处一个词,如「青云宗外门」);characters 只列**在场**的角色名(用正文里的名字,不要写「主角」)。",
|
|
9942
|
-
"5. event 一句话说清发生了什么(不超过 40 字)。",
|
|
9943
|
-
"输出必须是合法 JSON 数组,不要输出任何其他文字或 Markdown 代码块标记:",
|
|
9944
|
-
"[{\"time\":\"第三日黄昏\",\"order\":1,\"place\":\"青云宗外门\",\"characters\":[\"沈青\"],\"event\":\"沈青被外门弟子当众羞辱\"}]",
|
|
9945
|
-
"重要:字符串值内部不得包含换行符;JSON 必须在一段内完整结束。"
|
|
9946
|
-
].join("@NL@").replace("@NL@", NL),
|
|
10251
|
+
system: timelineExtractSystemPrompt(),
|
|
9947
10252
|
user: `第 ${chapterNo} 章《${chapter.title}》正文:@NL@@NL@${stripChapterHeadings(body).slice(0, 24e3)}`.replace("@NL@", NL),
|
|
9948
10253
|
temperature: .2,
|
|
9949
10254
|
maxTokens: Math.max(config.maxTokens, 4e3),
|
|
@@ -9986,15 +10291,7 @@ async function reviewTimeline(ctx, config, project) {
|
|
|
9986
10291
|
return "- " + parts.join(" ");
|
|
9987
10292
|
});
|
|
9988
10293
|
const text = await complete(ctx, config, {
|
|
9989
|
-
system:
|
|
9990
|
-
"你是小说连续性编辑,只做一件事:找**时间线**上的矛盾。",
|
|
9991
|
-
"检查项:1) 时间倒流(后一章早于前一章且没有明确插叙标注);2) 地点瞬移(同一人物无移动交代就换场景);",
|
|
9992
|
-
"3) 时间跨度与事件量明显不匹配(如「一日之内」发生跨越数月的剧情);4) 季节/年龄/节日前后矛盾;5) 回忆与现实混淆。",
|
|
9993
|
-
"只报**能在给定事件表里指认**的问题,不要臆测;没有问题就返回空数组。",
|
|
9994
|
-
"输出必须是合法 JSON 数组,不要任何其他文字:",
|
|
9995
|
-
"[{\"severity\":\"high|medium|low\",\"chapters\":[3,7],\"item\":\"问题一句话\",\"suggestion\":\"怎么修\"}]",
|
|
9996
|
-
"重要:字符串值内部不得包含换行符。"
|
|
9997
|
-
].join(NL),
|
|
10294
|
+
system: timelineReviewSystemPrompt(),
|
|
9998
10295
|
user: `故事时间线(按章节顺序):@NL@${lines.join(NL)}`.replace("@NL@", NL),
|
|
9999
10296
|
temperature: .2,
|
|
10000
10297
|
maxTokens: Math.max(config.maxTokens, 4e3),
|