@waterwx/dsh-novel-forge 2.3.0 → 2.5.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 +1373 -704
- package/lib/index.js +498 -28
- package/lib/types/assets.d.ts +3 -3
- package/lib/types/client/api.d.ts +18 -0
- package/lib/types/client/panel/NameForgeView.d.ts +11 -0
- package/lib/types/client/panel/StyleView.d.ts +4 -0
- package/lib/types/engine.d.ts +33 -1
- package/lib/types/name-forge.d.ts +110 -0
- package/lib/types/prompts/nameforge.d.ts +16 -0
- package/lib/types/protocol.d.ts +108 -0
- package/lib/types/style-fingerprint.d.ts +3 -2
- package/lib/types/style-profile.d.ts +62 -0
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -605,10 +605,11 @@ function formatMetric(metric, value) {
|
|
|
605
605
|
}
|
|
606
606
|
/**
|
|
607
607
|
* 渲染**生成端**的风格目标(区间形式)。
|
|
608
|
-
* @param reference -
|
|
608
|
+
* @param reference - 参照指纹(写法样本或蒸馏卡)。
|
|
609
|
+
* @param sourceLabel - 参照物的人类可读说明(缺省「绑定的写法样本」)。
|
|
609
610
|
* @returns 可注入写作 prompt 的文本;样本过短时返回空串。
|
|
610
611
|
*/
|
|
611
|
-
function renderFingerprintTargets(reference) {
|
|
612
|
+
function renderFingerprintTargets(reference, sourceLabel) {
|
|
612
613
|
if (reference.chars < MIN_CHARS) return "";
|
|
613
614
|
const items = [];
|
|
614
615
|
const add = (metric, suffix) => {
|
|
@@ -631,7 +632,7 @@ function renderFingerprintTargets(reference) {
|
|
|
631
632
|
add("imageryPer1000", " 次/千字");
|
|
632
633
|
if (items.length === 0) return "";
|
|
633
634
|
return [
|
|
634
|
-
"====================
|
|
635
|
+
"==================== 风格目标(确定性指标,取自" + (sourceLabel ?? "绑定的写法样本") + ") ====================",
|
|
635
636
|
"本章正文应落在这几个区间内。不是硬性红线,但明显越界会被审稿判为偏离绑定写法:",
|
|
636
637
|
...items
|
|
637
638
|
].join("\n");
|
|
@@ -3634,7 +3635,7 @@ function renderGenreAndProgression(assets) {
|
|
|
3634
3635
|
return sections.join("\n");
|
|
3635
3636
|
}
|
|
3636
3637
|
/** 渲染写法资产提示词块(规则去重,省 token)。 */
|
|
3637
|
-
function renderStyleAssets(assets) {
|
|
3638
|
+
function renderStyleAssets(assets, profile) {
|
|
3638
3639
|
const styles = assets?.styleAssets ?? [];
|
|
3639
3640
|
if (styles.length === 0) return "";
|
|
3640
3641
|
const sections = ["==================== 写法资产(本书的叙事风格约束) ===================="];
|
|
@@ -3645,7 +3646,10 @@ function renderStyleAssets(assets) {
|
|
|
3645
3646
|
if (style.dialogueRules.length > 0) sections.push("台词风格:\n" + unique(style.dialogueRules).map((r) => `- ${r}`).join("\n"));
|
|
3646
3647
|
if (style.descriptionRules.length > 0) sections.push("描写与情绪:\n" + unique(style.descriptionRules).map((r) => `- ${r}`).join("\n"));
|
|
3647
3648
|
if (style.boundaries.length > 0) sections.push("表达边界:\n" + unique(style.boundaries).map((r) => `- ${r}`).join("\n"));
|
|
3648
|
-
if (
|
|
3649
|
+
if (profile !== void 0) {
|
|
3650
|
+
const targets = renderFingerprintTargets(profile.fingerprint, profile.label);
|
|
3651
|
+
if (targets !== "") sections.push(targets);
|
|
3652
|
+
} else if (style.sourceText !== void 0 && style.sourceText !== "") {
|
|
3649
3653
|
const targets = renderFingerprintTargets(extractStyleFingerprint(style.sourceText));
|
|
3650
3654
|
if (targets !== "") sections.push(targets);
|
|
3651
3655
|
}
|
|
@@ -3653,10 +3657,10 @@ function renderStyleAssets(assets) {
|
|
|
3653
3657
|
return sections.join("\n");
|
|
3654
3658
|
}
|
|
3655
3659
|
/** 渲染全部写作资产提示词(供生成/规划/审稿注入)。 */
|
|
3656
|
-
function renderAllAssets(assets) {
|
|
3660
|
+
function renderAllAssets(assets, profile) {
|
|
3657
3661
|
return [
|
|
3658
3662
|
renderGenreAndProgression(assets),
|
|
3659
|
-
renderStyleAssets(assets),
|
|
3663
|
+
renderStyleAssets(assets, profile),
|
|
3660
3664
|
renderAntiAiRules(assets)
|
|
3661
3665
|
].filter((part) => part !== "").join("\n\n");
|
|
3662
3666
|
}
|
|
@@ -4576,6 +4580,278 @@ function detectTimelineIssues(project) {
|
|
|
4576
4580
|
return issues;
|
|
4577
4581
|
}
|
|
4578
4582
|
//#endregion
|
|
4583
|
+
//#region src/name-forge.ts
|
|
4584
|
+
/** 取名类别表(顺序即面板顺序)。 */
|
|
4585
|
+
const NAME_CATEGORIES = [
|
|
4586
|
+
{
|
|
4587
|
+
id: "person",
|
|
4588
|
+
label: "人名",
|
|
4589
|
+
hint: "主角、配角、反派、路人——姓与名都要有题材气质。",
|
|
4590
|
+
brief: "人物姓名(姓氏+名,或复姓/单名/字号),需有记忆点,避免烂大街的谐音梗",
|
|
4591
|
+
placeholder: "例如:冷僻复姓;主角名带孤勇感;反派名要雅致但有压迫感",
|
|
4592
|
+
maxCount: 20
|
|
4593
|
+
},
|
|
4594
|
+
{
|
|
4595
|
+
id: "place",
|
|
4596
|
+
label: "地名",
|
|
4597
|
+
hint: "宗门所在地、秘境、城池、山川——世界观的空间锚点。",
|
|
4598
|
+
brief: "地名(山川/城池/秘境/宗门所在),需与世界观地理层级自洽",
|
|
4599
|
+
placeholder: "例如:剑修宗门设在雪山;秘境名字要带蛊惑感",
|
|
4600
|
+
maxCount: 20
|
|
4601
|
+
},
|
|
4602
|
+
{
|
|
4603
|
+
id: "technique",
|
|
4604
|
+
label: "功法",
|
|
4605
|
+
hint: "招式、心法、神通、禁术——战力体系的门面。",
|
|
4606
|
+
brief: "功法/招式/神通名(心法、剑式、禁术),需体现运转机理或意境,避免空泛形容词堆砌",
|
|
4607
|
+
placeholder: "例如:偏雷法;名字要短促狠戾;禁术名字带反噬感",
|
|
4608
|
+
maxCount: 20
|
|
4609
|
+
},
|
|
4610
|
+
{
|
|
4611
|
+
id: "faction",
|
|
4612
|
+
label: "势力",
|
|
4613
|
+
hint: "宗门、家族、王朝、组织——派系斗争的名字牌。",
|
|
4614
|
+
brief: "势力/组织/宗门/家族名,需体现其立身之道与行事风格",
|
|
4615
|
+
placeholder: "例如:邪道宗门;名字要堂皇但暗藏戾气",
|
|
4616
|
+
maxCount: 20
|
|
4617
|
+
},
|
|
4618
|
+
{
|
|
4619
|
+
id: "artifact",
|
|
4620
|
+
label: "宝物",
|
|
4621
|
+
hint: "法宝、丹药、神器、材料——金手指与争夺战的载体。",
|
|
4622
|
+
brief: "宝物/法宝/丹药/材料名,需暗示品阶或异能,避免「XX神器」式敷衍",
|
|
4623
|
+
placeholder: "例如:主角本命法宝;名字与功法同源",
|
|
4624
|
+
maxCount: 20
|
|
4625
|
+
}
|
|
4626
|
+
];
|
|
4627
|
+
/** 类别 id → 规格。 */
|
|
4628
|
+
const CATEGORY_BY_ID = new Map(NAME_CATEGORIES.map((c) => [c.id, c]));
|
|
4629
|
+
/** 取一个类别规格;未知 id 抛错(路由层转 400)。 */
|
|
4630
|
+
function categorySpec(id) {
|
|
4631
|
+
const spec = CATEGORY_BY_ID.get(id);
|
|
4632
|
+
if (spec === void 0) throw new Error(`未知取名类别:${id}`);
|
|
4633
|
+
return spec;
|
|
4634
|
+
}
|
|
4635
|
+
/** 名字长度与理由长度上限(截断显式化)。 */
|
|
4636
|
+
const NAME_MAX = 30;
|
|
4637
|
+
const NOTE_MAX = 120;
|
|
4638
|
+
/** 归一化一条候选/入库条目(裁剪 + 截断;去重由调用方处理)。 */
|
|
4639
|
+
function normalizeNameEntry(raw) {
|
|
4640
|
+
return {
|
|
4641
|
+
name: typeof raw.name === "string" ? raw.name.replace(/\s+/g, " ").trim().slice(0, NAME_MAX) : "",
|
|
4642
|
+
note: typeof raw.note === "string" ? raw.note.replace(/\r?\n/g, " ").trim().slice(0, NOTE_MAX) : ""
|
|
4643
|
+
};
|
|
4644
|
+
}
|
|
4645
|
+
/**
|
|
4646
|
+
* 解析取名 LLM 输出为候选列表。
|
|
4647
|
+
*
|
|
4648
|
+
* 宽容度分级:标准 JSON → 去围栏 → 截取首尾花括号;仍失败返回空数组(由
|
|
4649
|
+
* 调用方决定重试或报错),绝不把模型废话当名字。
|
|
4650
|
+
*
|
|
4651
|
+
* @param text 模型原始输出。
|
|
4652
|
+
* @param category 目标类别(用于校验字段完整性)。
|
|
4653
|
+
* @param count 期望条数(上限)。
|
|
4654
|
+
* @returns 候选列表(已去重、去空、截断)。
|
|
4655
|
+
*/
|
|
4656
|
+
function parseNameCandidates(text, count) {
|
|
4657
|
+
const raw = extractJsonObject(text);
|
|
4658
|
+
if (raw === void 0) return [];
|
|
4659
|
+
const list = Array.isArray(raw.names) ? raw.names : [];
|
|
4660
|
+
const out = [];
|
|
4661
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4662
|
+
for (const item of list) {
|
|
4663
|
+
if (typeof item !== "object" || item === null) continue;
|
|
4664
|
+
const rec = item;
|
|
4665
|
+
const name = typeof rec.name === "string" ? rec.name.trim() : "";
|
|
4666
|
+
if (name === "" || seen.has(name)) continue;
|
|
4667
|
+
seen.add(name);
|
|
4668
|
+
const note = typeof rec.note === "string" ? rec.note.trim().slice(0, NOTE_MAX) : "";
|
|
4669
|
+
out.push({
|
|
4670
|
+
name: name.slice(0, NAME_MAX),
|
|
4671
|
+
note
|
|
4672
|
+
});
|
|
4673
|
+
if (out.length >= count) break;
|
|
4674
|
+
}
|
|
4675
|
+
return out;
|
|
4676
|
+
}
|
|
4677
|
+
/** 从模型输出里尽力挖出一个 JSON 对象(标准 / 围栏 / 截取三档)。 */
|
|
4678
|
+
function extractJsonObject(text) {
|
|
4679
|
+
const attempts = [text.trim(), text.replace(/^```(?:json)?\s*/i, "").replace(/```\s*$/, "").trim()];
|
|
4680
|
+
for (const attempt of attempts) try {
|
|
4681
|
+
const parsed = JSON.parse(attempt);
|
|
4682
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
|
|
4683
|
+
} catch {}
|
|
4684
|
+
const start = text.indexOf("{");
|
|
4685
|
+
const end = text.lastIndexOf("}");
|
|
4686
|
+
if (start !== -1 && end > start) try {
|
|
4687
|
+
const parsed = JSON.parse(text.slice(start, end + 1));
|
|
4688
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
|
|
4689
|
+
} catch {}
|
|
4690
|
+
}
|
|
4691
|
+
/**
|
|
4692
|
+
* 构造取名请求的 user 提示词。
|
|
4693
|
+
*
|
|
4694
|
+
* 书籍语境三件套:题材基调、世界设定(境界/区域/势力既有名)、已入库名字
|
|
4695
|
+
* (避重)——三者都可缺省,缺省时明确告诉模型「无既有设定,自由发挥」。
|
|
4696
|
+
*
|
|
4697
|
+
* @param category 类别规格。
|
|
4698
|
+
* @param count 期望条数。
|
|
4699
|
+
* @param hint 作者补充要求(可空)。
|
|
4700
|
+
* @param context 书籍语境。
|
|
4701
|
+
*/
|
|
4702
|
+
function buildNameForgeUserPrompt(category, count, hint, context) {
|
|
4703
|
+
const genre = (context.genre ?? "").trim();
|
|
4704
|
+
const world = (context.worldNames ?? []).filter((n) => n.trim() !== "").slice(0, 40);
|
|
4705
|
+
const bank = (context.bankNames ?? []).filter((n) => n.trim() !== "").slice(0, 60);
|
|
4706
|
+
return [
|
|
4707
|
+
`类别:${category.label}(${category.brief})`,
|
|
4708
|
+
`请生成 ${count} 个候选。`,
|
|
4709
|
+
genre !== "" ? `本书题材基调:${genre}——名字气质必须贴合。` : "(未提供题材基调,按通用中文网文气质发挥)",
|
|
4710
|
+
world.length > 0 ? "世界观既有名称(新名字须与它们同一语感,不得重复或近音):\n" + world.map((n) => `- ${n}`).join("\n") : "(暂无世界观设定,自由发挥)",
|
|
4711
|
+
bank.length > 0 ? "已入库名字(一个都不得重复或变体):\n" + bank.map((n) => `- ${n}`).join("\n") : "",
|
|
4712
|
+
hint.trim() !== "" ? `作者补充要求:${hint.trim().slice(0, 300)}` : "",
|
|
4713
|
+
"输出必须是合法 JSON 对象:{\"names\":[{\"name\":\"名字\",\"note\":\"命名理由/意象(20字内)\"}]}。"
|
|
4714
|
+
].filter((s) => s !== "").join("\n\n");
|
|
4715
|
+
}
|
|
4716
|
+
/** 从项目状态收集取名语境(题材 + 世界既有名 + 已入库名字)。 */
|
|
4717
|
+
function collectNameForgeContext(project) {
|
|
4718
|
+
const genre = project.assets?.genre?.name ?? project.bible?.genre ?? "";
|
|
4719
|
+
const worldNames = [];
|
|
4720
|
+
for (const realm of project.world?.realms ?? []) if (realm.name.trim() !== "") worldNames.push(realm.name.trim());
|
|
4721
|
+
for (const region of project.world?.regions ?? []) if (region.name.trim() !== "") worldNames.push(region.name.trim());
|
|
4722
|
+
for (const faction of project.world?.factions ?? []) if (faction.name.trim() !== "") worldNames.push(faction.name.trim());
|
|
4723
|
+
for (const role of project.roles ?? []) if (role.name.trim() !== "") worldNames.push(role.name.trim());
|
|
4724
|
+
for (const card of project.bible?.characters ?? []) if (card.name.trim() !== "") worldNames.push(card.name.trim());
|
|
4725
|
+
return {
|
|
4726
|
+
genre,
|
|
4727
|
+
worldNames,
|
|
4728
|
+
bankNames: readNameBank(project).map((e) => e.name)
|
|
4729
|
+
};
|
|
4730
|
+
}
|
|
4731
|
+
/** 读取并规整名字库(按类别分组返回前先做一致性校验;非法条目丢弃)。 */
|
|
4732
|
+
function readNameBank(project) {
|
|
4733
|
+
const raw = Array.isArray(project.nameBank) ? project.nameBank : [];
|
|
4734
|
+
const out = [];
|
|
4735
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4736
|
+
for (const item of raw) {
|
|
4737
|
+
if (typeof item !== "object" || item === null) continue;
|
|
4738
|
+
const rec = item;
|
|
4739
|
+
const category = typeof rec.category === "string" ? rec.category : "";
|
|
4740
|
+
if (!CATEGORY_BY_ID.has(category)) continue;
|
|
4741
|
+
const name = typeof rec.name === "string" ? rec.name.trim() : "";
|
|
4742
|
+
if (name === "") continue;
|
|
4743
|
+
const key = `${category}:${name}`;
|
|
4744
|
+
if (seen.has(key)) continue;
|
|
4745
|
+
seen.add(key);
|
|
4746
|
+
out.push({
|
|
4747
|
+
id: typeof rec.id === "string" && rec.id !== "" ? rec.id : `${category}:${name}`,
|
|
4748
|
+
category,
|
|
4749
|
+
name: name.slice(0, NAME_MAX),
|
|
4750
|
+
note: typeof rec.note === "string" ? rec.note.slice(0, NOTE_MAX) : "",
|
|
4751
|
+
adopted: rec.adopted !== false,
|
|
4752
|
+
createdAt: typeof rec.createdAt === "string" ? rec.createdAt : (/* @__PURE__ */ new Date(0)).toISOString()
|
|
4753
|
+
});
|
|
4754
|
+
}
|
|
4755
|
+
return out;
|
|
4756
|
+
}
|
|
4757
|
+
/**
|
|
4758
|
+
* 写入名字库(采纳候选 / 弃用单条 / 清空)。
|
|
4759
|
+
*
|
|
4760
|
+
* 容量纪律:单类 ≤ 60、总数 ≤ 200;超限抛错由路由转 400,**不做静默截断**——
|
|
4761
|
+
* 让作者明确知道库满了,而不是疑惑「我刚取的名字去哪了」。
|
|
4762
|
+
*/
|
|
4763
|
+
function mutateNameBank(project, op) {
|
|
4764
|
+
const current = readNameBank(project);
|
|
4765
|
+
let next;
|
|
4766
|
+
if (op.kind === "adopt") {
|
|
4767
|
+
const existing = new Set(current.map((e) => `${e.category}:${e.name}`));
|
|
4768
|
+
const fresh = op.candidates.map((c) => normalizeNameEntry(c)).filter((n) => n.name !== "" && !existing.has(`${op.category}:${n.name}`)).map((n) => ({
|
|
4769
|
+
id: `${op.category}:${n.name}`,
|
|
4770
|
+
category: op.category,
|
|
4771
|
+
name: n.name,
|
|
4772
|
+
note: n.note,
|
|
4773
|
+
adopted: true,
|
|
4774
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4775
|
+
}));
|
|
4776
|
+
if (fresh.length === 0) return current;
|
|
4777
|
+
next = [...current, ...fresh];
|
|
4778
|
+
} else if (op.kind === "discard") next = current.filter((e) => e.id !== op.id);
|
|
4779
|
+
else next = [];
|
|
4780
|
+
const perCategory = /* @__PURE__ */ new Map();
|
|
4781
|
+
for (const e of next) perCategory.set(e.category, (perCategory.get(e.category) ?? 0) + 1);
|
|
4782
|
+
for (const [category, n] of perCategory) if (n > 60) throw new Error(`名字库「${categorySpec(category).label}」已满(${n}/60),请先清理再采纳`);
|
|
4783
|
+
if (next.length > 200) throw new Error(`名字库已满(${next.length}/200),请先清理再采纳`);
|
|
4784
|
+
project.nameBank = next;
|
|
4785
|
+
return next;
|
|
4786
|
+
}
|
|
4787
|
+
/**
|
|
4788
|
+
* 把已采纳名字渲染成写作提示词块(本书层注入)。
|
|
4789
|
+
*
|
|
4790
|
+
* 声明边界:优先使用、不锁死——「表外创新名须与下表气质一致」。
|
|
4791
|
+
*
|
|
4792
|
+
* @param project 项目状态。
|
|
4793
|
+
* @returns 提示词片段;无库时返回空串。
|
|
4794
|
+
*/
|
|
4795
|
+
function renderNameBank(project) {
|
|
4796
|
+
const entries = readNameBank(project).filter((e) => e.adopted);
|
|
4797
|
+
if (entries.length === 0) return "";
|
|
4798
|
+
const lines = ["==================== 已定名资产(优先使用,不得另起同名) ===================="];
|
|
4799
|
+
for (const spec of NAME_CATEGORIES) {
|
|
4800
|
+
const names = entries.filter((e) => e.category === spec.id).map((e) => e.name);
|
|
4801
|
+
if (names.length === 0) continue;
|
|
4802
|
+
lines.push(`${spec.label}:${names.join("、")}`);
|
|
4803
|
+
}
|
|
4804
|
+
lines.push("正文涉及上表类别的新专有名字,优先直接从表中取;表外创新名须与上表同一语感,不得与上表近音或重复。");
|
|
4805
|
+
return lines.join("\n");
|
|
4806
|
+
}
|
|
4807
|
+
//#endregion
|
|
4808
|
+
//#region src/style-profile.ts
|
|
4809
|
+
/**
|
|
4810
|
+
* 解析生成端生效的风格目标。
|
|
4811
|
+
*
|
|
4812
|
+
* 优先级:显式卡 > 绑定资产的样本文本 > 无。样本文本取「第一个有 sourceText
|
|
4813
|
+
* 的资产」,指纹由调用方现算——本模块不依赖 style-fingerprint,保持纯数据。
|
|
4814
|
+
*
|
|
4815
|
+
* @param project 项目状态(只需 styleProfile 与 assets.styleAssets[].sourceText)。
|
|
4816
|
+
* @returns 生效目标。
|
|
4817
|
+
*/
|
|
4818
|
+
function resolveStyleTarget(project) {
|
|
4819
|
+
if (project.styleProfile !== void 0) return {
|
|
4820
|
+
kind: "profile",
|
|
4821
|
+
fingerprint: project.styleProfile.fingerprint,
|
|
4822
|
+
label: project.styleProfile.label
|
|
4823
|
+
};
|
|
4824
|
+
for (const style of project.assets?.styleAssets ?? []) {
|
|
4825
|
+
const sample = style.sourceText;
|
|
4826
|
+
if (sample !== void 0 && sample !== "") return {
|
|
4827
|
+
kind: "sample",
|
|
4828
|
+
sampleText: sample,
|
|
4829
|
+
label: "绑定写法资产的样本文本"
|
|
4830
|
+
};
|
|
4831
|
+
}
|
|
4832
|
+
return { kind: "none" };
|
|
4833
|
+
}
|
|
4834
|
+
/** 用指纹数据构造一张目标卡。 */
|
|
4835
|
+
function makeStyleProfile(source, label, fingerprint, sampleChapters) {
|
|
4836
|
+
return {
|
|
4837
|
+
source,
|
|
4838
|
+
label,
|
|
4839
|
+
fingerprint,
|
|
4840
|
+
...sampleChapters !== void 0 ? { sampleChapters } : {},
|
|
4841
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4842
|
+
};
|
|
4843
|
+
}
|
|
4844
|
+
/**
|
|
4845
|
+
* 校验一次 distilled-sample 请求的样本文本。
|
|
4846
|
+
* @returns 归一化后的文本。
|
|
4847
|
+
* @throws 文本过短时抛错(路由转 400)。
|
|
4848
|
+
*/
|
|
4849
|
+
function normalizeSampleText(text) {
|
|
4850
|
+
const value = (text ?? "").replace(/\r\n/g, "\n").trim();
|
|
4851
|
+
if (value.length < 200) throw new Error("样本文本过短(<200 字符),指标没有统计意义");
|
|
4852
|
+
return value.slice(0, 2e4);
|
|
4853
|
+
}
|
|
4854
|
+
//#endregion
|
|
4579
4855
|
//#region src/snapshots.ts
|
|
4580
4856
|
/**
|
|
4581
4857
|
* 章节历史版本(快照):覆盖写入前自动留存正文,支持列表 / 回滚 / 清理。
|
|
@@ -5587,10 +5863,12 @@ function writeSystemPrompt(project, targetChars, lengthRule, beforeChapter) {
|
|
|
5587
5863
|
const outlineBlock = project.outline.length > 6e3 ? project.outline.slice(0, 6e3) + "\n…(大纲过长已节选,完整内容见总纲页)" : project.outline;
|
|
5588
5864
|
book.push(outlineBlock);
|
|
5589
5865
|
book.push("==================== 大纲结束 ====================");
|
|
5590
|
-
const assetsBlock = renderAllAssets(project.assets);
|
|
5866
|
+
const assetsBlock = renderAllAssets(project.assets, project.styleProfile);
|
|
5591
5867
|
if (assetsBlock !== "") book.push(assetsBlock);
|
|
5592
5868
|
const slotBlock = renderPromptSlots(project);
|
|
5593
5869
|
if (slotBlock !== "") book.push(slotBlock);
|
|
5870
|
+
const nameBlock = renderNameBank(project);
|
|
5871
|
+
if (nameBlock !== "") book.push(nameBlock);
|
|
5594
5872
|
blocks.push({
|
|
5595
5873
|
layer: "book",
|
|
5596
5874
|
text: book.join("\n")
|
|
@@ -5680,7 +5958,7 @@ function reviewSystemPrompt(project) {
|
|
|
5680
5958
|
text: fixed.join("\n")
|
|
5681
5959
|
});
|
|
5682
5960
|
const book = [];
|
|
5683
|
-
const assetsBlock = renderAllAssets(project.assets);
|
|
5961
|
+
const assetsBlock = renderAllAssets(project.assets, project.styleProfile);
|
|
5684
5962
|
if (assetsBlock !== "") book.push("\n" + assetsBlock);
|
|
5685
5963
|
if (bible !== void 0) {
|
|
5686
5964
|
book.push("\n==================== 道藏 ====================");
|
|
@@ -5774,7 +6052,7 @@ function authorReviewSystemPrompt() {
|
|
|
5774
6052
|
//#region src/prompts/polish.ts
|
|
5775
6053
|
/** The de-AI-ify polish system prompt (with project writing assets injected). */
|
|
5776
6054
|
function polishSystemPrompt(project) {
|
|
5777
|
-
const assetsBlock = renderAllAssets(project.assets);
|
|
6055
|
+
const assetsBlock = renderAllAssets(project.assets, project.styleProfile);
|
|
5778
6056
|
const bible = project.bible;
|
|
5779
6057
|
const blocks = [];
|
|
5780
6058
|
const fixed = [
|
|
@@ -6281,6 +6559,20 @@ function blurbSystemPrompt() {
|
|
|
6281
6559
|
].join("\n");
|
|
6282
6560
|
}
|
|
6283
6561
|
//#endregion
|
|
6562
|
+
//#region src/prompts/nameforge.ts
|
|
6563
|
+
/** 取名系统提示词:类别感知 + 避重 + 结构化 JSON 输出。 */
|
|
6564
|
+
function nameForgeSystemPrompt() {
|
|
6565
|
+
return [
|
|
6566
|
+
"你是资深中文网文命名师,擅长按题材气质取「读一遍就记得住」的专有名字。",
|
|
6567
|
+
"硬规则:",
|
|
6568
|
+
"1. 只输出 JSON 对象,格式 {\"names\":[{\"name\":\"名字\",\"note\":\"命名理由/意象(20字内)\"}]},不要输出其他文字;",
|
|
6569
|
+
"2. 名字必须是中文(人名2-4字、地名2-6字、功法3-7字、势力2-8字、宝物2-8字),不得出现外文、拼音、生僻怪字;",
|
|
6570
|
+
"3. 严格避雷:不得使用真实存在的明星、品牌、企业、敏感人物名;不套用其它作品的成名专名;",
|
|
6571
|
+
"4. 每批名字内部不得互相近音、近形;若给出「避用名单」,其中名字一个都不得再产出;",
|
|
6572
|
+
"5. note 只写命名思路(取自什么意象/气质),不复述名字本身。"
|
|
6573
|
+
].join("\n");
|
|
6574
|
+
}
|
|
6575
|
+
//#endregion
|
|
6284
6576
|
//#region src/protocol.ts
|
|
6285
6577
|
/**
|
|
6286
6578
|
* dsh-novel-forge — shared protocol between the host half (Node) and the
|
|
@@ -6318,8 +6610,12 @@ const NOVEL_API = {
|
|
|
6318
6610
|
/** 张力曲线(GET 数据 / POST 设置)。 */
|
|
6319
6611
|
tension: "/api/dsh-novel-forge/tension",
|
|
6320
6612
|
styleHistory: "/api/dsh-novel-forge/style-history",
|
|
6613
|
+
/** 风格目标卡:GET 当前卡;POST {action:'distill-archived'|'distill-sample'|'clear'}。 */
|
|
6614
|
+
styleProfile: "/api/dsh-novel-forge/style-profile",
|
|
6321
6615
|
/** 提示词槽位(GET 列表 / POST 写入)。 */
|
|
6322
6616
|
promptSlots: "/api/dsh-novel-forge/prompt-slots",
|
|
6617
|
+
/** 取名器:POST {action:'generate'} 生成候选;GET 名字库;POST {action:'adopt'|'discard'|'clear'} 维护。 */
|
|
6618
|
+
nameForge: "/api/dsh-novel-forge/name-forge",
|
|
6323
6619
|
/** 故事时间线(GET 列表 / POST 操作)。 */
|
|
6324
6620
|
timeline: "/api/dsh-novel-forge/timeline",
|
|
6325
6621
|
marketRadar: "/api/dsh-novel-forge/market-radar",
|
|
@@ -7555,24 +7851,11 @@ const STYLE_BASELINE_MIN_CHAPTERS = 3;
|
|
|
7555
7851
|
* @returns 基线;既无已过审章节也无带样本文本的写法资产时返回 undefined。
|
|
7556
7852
|
*/
|
|
7557
7853
|
function styleBaseline(outputDir, project) {
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
const samples = [];
|
|
7564
|
-
for (const chapter of picked) {
|
|
7565
|
-
const raw = readChapterFile(outputDir, chapter);
|
|
7566
|
-
if (raw === void 0) continue;
|
|
7567
|
-
const fp = extractStyleFingerprint(stripChapterHeadings(raw));
|
|
7568
|
-
if (fp.sentences > 0) samples.push(fp);
|
|
7569
|
-
}
|
|
7570
|
-
if (samples.length >= STYLE_BASELINE_MIN_CHAPTERS) return {
|
|
7571
|
-
fingerprint: mergeFingerprints(samples),
|
|
7572
|
-
label: "本书已过审 " + samples.length + " 章的中位数"
|
|
7573
|
-
};
|
|
7574
|
-
}
|
|
7575
|
-
}
|
|
7854
|
+
const archived = sampleArchivedFingerprint(outputDir, project);
|
|
7855
|
+
if (archived !== void 0) return {
|
|
7856
|
+
fingerprint: archived.fingerprint,
|
|
7857
|
+
label: "本书已过审 " + archived.count + " 章的中位数"
|
|
7858
|
+
};
|
|
7576
7859
|
for (const style of project.assets?.styleAssets ?? []) {
|
|
7577
7860
|
const sample = style.sourceText;
|
|
7578
7861
|
if (sample === void 0 || sample === "") continue;
|
|
@@ -7584,6 +7867,51 @@ function styleBaseline(outputDir, project) {
|
|
|
7584
7867
|
};
|
|
7585
7868
|
}
|
|
7586
7869
|
}
|
|
7870
|
+
/**
|
|
7871
|
+
* 从本书**已过审章节**等距抽样算风格指纹(中位数)。
|
|
7872
|
+
*
|
|
7873
|
+
* 审稿尺子(styleBaseline)与蒸馏卡(distillStyleProfile)共用这一份采样——
|
|
7874
|
+
* 两条路必须同源,否则「审稿说没偏离、生成目标却是另一个方向」。
|
|
7875
|
+
* @returns 抽样指纹与参与章数;不足 {@link STYLE_BASELINE_MIN_CHAPTERS} 章时返回 undefined。
|
|
7876
|
+
*/
|
|
7877
|
+
function sampleArchivedFingerprint(outputDir, project) {
|
|
7878
|
+
if (outputDir === void 0) return void 0;
|
|
7879
|
+
const approved = project.chapters.filter((c) => c.status === "approved" && c.file !== "");
|
|
7880
|
+
if (approved.length < STYLE_BASELINE_MIN_CHAPTERS) return void 0;
|
|
7881
|
+
const step = Math.max(1, Math.floor(approved.length / STYLE_BASELINE_CHAPTERS));
|
|
7882
|
+
const picked = approved.filter((_, index) => index % step === 0).slice(0, STYLE_BASELINE_CHAPTERS);
|
|
7883
|
+
const samples = [];
|
|
7884
|
+
for (const chapter of picked) {
|
|
7885
|
+
const raw = readChapterFile(outputDir, chapter);
|
|
7886
|
+
if (raw === void 0) continue;
|
|
7887
|
+
const fp = extractStyleFingerprint(stripChapterHeadings(raw));
|
|
7888
|
+
if (fp.sentences > 0) samples.push(fp);
|
|
7889
|
+
}
|
|
7890
|
+
if (samples.length < STYLE_BASELINE_MIN_CHAPTERS) return void 0;
|
|
7891
|
+
return {
|
|
7892
|
+
fingerprint: mergeFingerprints(samples),
|
|
7893
|
+
count: samples.length
|
|
7894
|
+
};
|
|
7895
|
+
}
|
|
7896
|
+
/**
|
|
7897
|
+
* 蒸馏一张风格目标卡(作者显式动作,落 project.styleProfile)。
|
|
7898
|
+
*
|
|
7899
|
+
* archived:本书已过审章节的中位数(把「我现在实际写出来的样子」固化成目标);
|
|
7900
|
+
* sample:作者粘贴的参考样文本(开书定调)。
|
|
7901
|
+
*
|
|
7902
|
+
* 与审稿尺子刻意分工:见 style-profile.ts 文件头「棘轮效应」说明。
|
|
7903
|
+
* @throws 数据不足时抛错(路由转 400)。
|
|
7904
|
+
*/
|
|
7905
|
+
function distillStyleProfile(outputDir, project, source, sampleText) {
|
|
7906
|
+
if (source === "archived") {
|
|
7907
|
+
const archived = sampleArchivedFingerprint(outputDir, project);
|
|
7908
|
+
if (archived === void 0) throw new Error(`已过审章节不足 ${STYLE_BASELINE_MIN_CHAPTERS} 章,不够蒸馏(先写几章并过审)`);
|
|
7909
|
+
return makeStyleProfile("archived", `本书已过审 ${archived.count} 章的中位数`, archived.fingerprint, archived.count);
|
|
7910
|
+
}
|
|
7911
|
+
const fingerprint = extractStyleFingerprint(normalizeSampleText(sampleText));
|
|
7912
|
+
if (fingerprint.sentences === 0) throw new Error("样本文本无法抽取指标,请换一份更完整的正文");
|
|
7913
|
+
return makeStyleProfile("sample", `参考样本蒸馏(${fingerprint.chars} 字)`, fingerprint);
|
|
7914
|
+
}
|
|
7587
7915
|
function styleFidelityBlock(project, bodyText, outputDir) {
|
|
7588
7916
|
const baseline = styleBaseline(outputDir, project);
|
|
7589
7917
|
if (baseline === void 0) return "";
|
|
@@ -10156,6 +10484,26 @@ async function runIdeaInspiration(ctx, config, req) {
|
|
|
10156
10484
|
})) : [];
|
|
10157
10485
|
return { ideas: arr(raw.ideas).slice(0, count) };
|
|
10158
10486
|
}
|
|
10487
|
+
/**
|
|
10488
|
+
* 取名器:按类别批量生成专有名字(题材感知 + 世界观避重 + 库内避重)。
|
|
10489
|
+
*
|
|
10490
|
+
* 候选**不落盘**——作者在面板逐条采纳后才进名字库(走 nameForgeRoute 的
|
|
10491
|
+
* adopt 动作)。生成失败(含模型输出无法解析)抛错,由路由转 500。
|
|
10492
|
+
*/
|
|
10493
|
+
async function runNameForge(ctx, config, project, req) {
|
|
10494
|
+
const spec = categorySpec(req.category);
|
|
10495
|
+
const count = Math.max(1, Math.min(spec.maxCount, Math.floor(req.count ?? 10)));
|
|
10496
|
+
const context = collectNameForgeContext(project);
|
|
10497
|
+
const candidates = parseNameCandidates(await complete(ctx, config, {
|
|
10498
|
+
system: nameForgeSystemPrompt(),
|
|
10499
|
+
user: buildNameForgeUserPrompt(spec, count, req.hint ?? "", context),
|
|
10500
|
+
temperature: .9,
|
|
10501
|
+
maxTokens: Math.max(config.maxTokens, 2e3),
|
|
10502
|
+
liveLabel: `取名·${spec.label}`
|
|
10503
|
+
}), count);
|
|
10504
|
+
if (candidates.length === 0) throw new Error("取名失败:模型输出无法解析,请重试或调整补充要求");
|
|
10505
|
+
return candidates;
|
|
10506
|
+
}
|
|
10159
10507
|
/** 雷达→灵感:基于市场信号/生产底座/创意简报,生成贴合市场的开书灵感。 */
|
|
10160
10508
|
async function runMarketIdeaInspiration(ctx, config, req) {
|
|
10161
10509
|
const count = Math.max(1, Math.min(10, req.count ?? 5));
|
|
@@ -14539,6 +14887,66 @@ function makeRoutes(deps) {
|
|
|
14539
14887
|
});
|
|
14540
14888
|
}
|
|
14541
14889
|
};
|
|
14890
|
+
/**
|
|
14891
|
+
* 风格目标卡:GET = 当前卡与生效说明;POST = 蒸馏 / 清空。
|
|
14892
|
+
*
|
|
14893
|
+
* 与 styleHistory 的分工:那条是**派生视图**(即时算、不落盘);本卡是作者
|
|
14894
|
+
* 显式决定的生成端锚点,落 project.styleProfile。详见 style-profile.ts 文件头。
|
|
14895
|
+
*/
|
|
14896
|
+
const styleProfileRoute = {
|
|
14897
|
+
kind: "exact",
|
|
14898
|
+
path: NOVEL_API.styleProfile,
|
|
14899
|
+
handler: async (req, res) => {
|
|
14900
|
+
const config = getConfig();
|
|
14901
|
+
const qurl = new URL(req.url ?? "/", "http://localhost");
|
|
14902
|
+
if (req.method === "GET") {
|
|
14903
|
+
if (!isLoopbackRequest(req)) {
|
|
14904
|
+
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
14905
|
+
return;
|
|
14906
|
+
}
|
|
14907
|
+
const project = loadProject(resolveOutputDir(config, qurl.searchParams.get("bookId") ?? void 0));
|
|
14908
|
+
if (project === void 0) {
|
|
14909
|
+
writeJson(res, 400, { error: "输出目录中没有项目" });
|
|
14910
|
+
return;
|
|
14911
|
+
}
|
|
14912
|
+
const target = resolveStyleTarget(project);
|
|
14913
|
+
writeJson(res, 200, {
|
|
14914
|
+
profile: project.styleProfile ?? null,
|
|
14915
|
+
approvedChapters: project.chapters.filter((c) => c.status === "approved" && c.file !== "").length,
|
|
14916
|
+
effectiveLabel: target.kind === "none" ? "无(未绑定带样本的写法资产)" : target.label
|
|
14917
|
+
});
|
|
14918
|
+
return;
|
|
14919
|
+
}
|
|
14920
|
+
if (!guard(req, res, "POST")) return;
|
|
14921
|
+
const body = await readJsonBody(req);
|
|
14922
|
+
if (body === void 0) {
|
|
14923
|
+
writeJson(res, 400, { error: "请求体缺失" });
|
|
14924
|
+
return;
|
|
14925
|
+
}
|
|
14926
|
+
const outputDir = resolveOutputDir(config, body.bookId);
|
|
14927
|
+
const project = loadProject(outputDir);
|
|
14928
|
+
if (project === void 0) {
|
|
14929
|
+
writeJson(res, 400, { error: "输出目录中没有项目" });
|
|
14930
|
+
return;
|
|
14931
|
+
}
|
|
14932
|
+
try {
|
|
14933
|
+
if (body.action === "clear") {
|
|
14934
|
+
delete project.styleProfile;
|
|
14935
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
14936
|
+
saveProject(outputDir, project);
|
|
14937
|
+
writeJson(res, 200, { profile: null });
|
|
14938
|
+
return;
|
|
14939
|
+
}
|
|
14940
|
+
const profile = distillStyleProfile(outputDir, project, body.action === "distill-archived" ? "archived" : "sample", body.text);
|
|
14941
|
+
project.styleProfile = profile;
|
|
14942
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
14943
|
+
saveProject(outputDir, project);
|
|
14944
|
+
writeJson(res, 200, { profile });
|
|
14945
|
+
} catch (error) {
|
|
14946
|
+
writeJson(res, 400, { error: error.message });
|
|
14947
|
+
}
|
|
14948
|
+
}
|
|
14949
|
+
};
|
|
14542
14950
|
/** 热门题材雷达:输入平台/题材/榜单文本 → 信号 + 生产底座 + 开书创意。 */
|
|
14543
14951
|
const marketRadarRoute = {
|
|
14544
14952
|
kind: "exact",
|
|
@@ -15302,6 +15710,66 @@ function makeRoutes(deps) {
|
|
|
15302
15710
|
});
|
|
15303
15711
|
}
|
|
15304
15712
|
};
|
|
15713
|
+
/**
|
|
15714
|
+
* 取名器:GET = 本书名字库;POST {action:'generate'} = 生成候选(不落盘);
|
|
15715
|
+
* POST {action:'adopt'|'discard'|'clear'} = 库维护。
|
|
15716
|
+
*/
|
|
15717
|
+
const nameForgeRoute = {
|
|
15718
|
+
kind: "exact",
|
|
15719
|
+
path: NOVEL_API.nameForge,
|
|
15720
|
+
handler: async (req, res) => {
|
|
15721
|
+
const config = getConfig();
|
|
15722
|
+
const qurl = new URL(req.url ?? "/", "http://localhost");
|
|
15723
|
+
if (req.method === "GET") {
|
|
15724
|
+
if (!isLoopbackRequest(req)) {
|
|
15725
|
+
writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
15726
|
+
return;
|
|
15727
|
+
}
|
|
15728
|
+
const project = loadProject(resolveOutputDir(config, qurl.searchParams.get("bookId") ?? void 0));
|
|
15729
|
+
if (project === void 0) {
|
|
15730
|
+
writeJson(res, 400, { error: "输出目录中没有项目" });
|
|
15731
|
+
return;
|
|
15732
|
+
}
|
|
15733
|
+
writeJson(res, 200, { entries: readNameBank(project) });
|
|
15734
|
+
return;
|
|
15735
|
+
}
|
|
15736
|
+
if (!guard(req, res, "POST")) return;
|
|
15737
|
+
const body = await readJsonBody(req);
|
|
15738
|
+
if (body === void 0) {
|
|
15739
|
+
writeJson(res, 400, { error: "请求体缺失" });
|
|
15740
|
+
return;
|
|
15741
|
+
}
|
|
15742
|
+
const outputDir = resolveOutputDir(config, body.bookId);
|
|
15743
|
+
const project = loadProject(outputDir);
|
|
15744
|
+
if (project === void 0) {
|
|
15745
|
+
writeJson(res, 400, { error: "输出目录中没有项目" });
|
|
15746
|
+
return;
|
|
15747
|
+
}
|
|
15748
|
+
try {
|
|
15749
|
+
if (body.action === "generate") {
|
|
15750
|
+
writeJson(res, 200, { candidates: await runNameForge(ctx, config, project, {
|
|
15751
|
+
category: body.category,
|
|
15752
|
+
count: body.count,
|
|
15753
|
+
hint: body.hint
|
|
15754
|
+
}) });
|
|
15755
|
+
return;
|
|
15756
|
+
}
|
|
15757
|
+
const entries = mutateNameBank(project, body.action === "adopt" ? {
|
|
15758
|
+
kind: "adopt",
|
|
15759
|
+
category: body.category ?? "person",
|
|
15760
|
+
candidates: body.candidates ?? []
|
|
15761
|
+
} : body.action === "discard" ? {
|
|
15762
|
+
kind: "discard",
|
|
15763
|
+
id: String(body.id ?? "")
|
|
15764
|
+
} : { kind: "clear" });
|
|
15765
|
+
project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
15766
|
+
saveProject(outputDir, project);
|
|
15767
|
+
writeJson(res, 200, { entries });
|
|
15768
|
+
} catch (error) {
|
|
15769
|
+
writeJson(res, 400, { error: error.message });
|
|
15770
|
+
}
|
|
15771
|
+
}
|
|
15772
|
+
};
|
|
15305
15773
|
const bookshelfRoute = {
|
|
15306
15774
|
kind: "exact",
|
|
15307
15775
|
path: NOVEL_API.bookshelf,
|
|
@@ -17051,6 +17519,7 @@ function makeRoutes(deps) {
|
|
|
17051
17519
|
marketRadarBriefRoute,
|
|
17052
17520
|
knowledgeRoute,
|
|
17053
17521
|
bookAnalysisRoute,
|
|
17522
|
+
nameForgeRoute,
|
|
17054
17523
|
ideaInspirationRoute,
|
|
17055
17524
|
marketIdeaInspirationRoute,
|
|
17056
17525
|
directorRoute,
|
|
@@ -17073,6 +17542,7 @@ function makeRoutes(deps) {
|
|
|
17073
17542
|
bookshelfImportTextRoute,
|
|
17074
17543
|
bookshelfImportTextPreviewRoute,
|
|
17075
17544
|
styleHistoryRoute,
|
|
17545
|
+
styleProfileRoute,
|
|
17076
17546
|
resetRoute,
|
|
17077
17547
|
auditRoute,
|
|
17078
17548
|
charactersRefreshRoute,
|
package/lib/types/assets.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Assets persist with the project (novel-project.json) and are injected into
|
|
9
9
|
* generation / planning / review prompts, and available to the AI assistant.
|
|
10
10
|
*/
|
|
11
|
-
import type { AntiAiRule, GenreNode, PlotBeatTemplate, ProgressionMode, ProjectAssets, StarterStyleProfile, StyleAsset, StyleTemplate } from './protocol.ts';
|
|
11
|
+
import type { AntiAiRule, GenreNode, PlotBeatTemplate, ProgressionMode, ProjectAssets, StarterStyleProfile, StyleAsset, StyleProfile, StyleTemplate } from './protocol.ts';
|
|
12
12
|
/** 预置叙事风格模板(8 套,一键绑定无需样本文本)。 */
|
|
13
13
|
export declare const BUILTIN_STYLE_TEMPLATES: StyleTemplate[];
|
|
14
14
|
/** 内置全局反 AI 规则(12 条,随插件发布;项目可叠加自定义规则)。 */
|
|
@@ -31,9 +31,9 @@ export declare function renderAntiAiRules(assets: ProjectAssets | undefined): st
|
|
|
31
31
|
/** 渲染题材与推进模式提示词块。 */
|
|
32
32
|
export declare function renderGenreAndProgression(assets: ProjectAssets | undefined): string;
|
|
33
33
|
/** 渲染写法资产提示词块(规则去重,省 token)。 */
|
|
34
|
-
export declare function renderStyleAssets(assets: ProjectAssets | undefined): string;
|
|
34
|
+
export declare function renderStyleAssets(assets: ProjectAssets | undefined, profile?: StyleProfile): string;
|
|
35
35
|
/** 渲染全部写作资产提示词(供生成/规划/审稿注入)。 */
|
|
36
|
-
export declare function renderAllAssets(assets: ProjectAssets | undefined): string;
|
|
36
|
+
export declare function renderAllAssets(assets: ProjectAssets | undefined, profile?: StyleProfile): string;
|
|
37
37
|
/** 预置写法模板 → 可直接绑定的 StyleAsset。 */
|
|
38
38
|
export declare function styleTemplateToAsset(template: StyleTemplate): StyleAsset;
|
|
39
39
|
/** 起始风格画像库:无样本文本也能快速绑定一套写法(4 档起步写法)。 */
|