dsh-novel-writer 3.7.0 → 3.8.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/lib/client.js +1 -31
- package/lib/index.js +119 -8
- package/lib/prompts.js +4 -1
- package/package.json +1 -1
- package/skills/novel-writing/SKILL.md +12 -8
package/lib/client.js
CHANGED
|
@@ -966,33 +966,6 @@ var css = `
|
|
|
966
966
|
var dirs = state.dirs || {};
|
|
967
967
|
var f = state.features || {};
|
|
968
968
|
var tools = state.tools || {};
|
|
969
|
-
var mergedFeatures = [
|
|
970
|
-
{ key: "emotionDeep", keys: ["emotionCaveat", "emotionComplexity"], labelKey: "feature.emotionDeep", descKey: "feature.emotionDeep.desc" },
|
|
971
|
-
{ key: "styleDetect", keys: ["genreTheme", "webnovelVibe"], labelKey: "feature.styleDetect", descKey: "feature.styleDetect.desc" }
|
|
972
|
-
];
|
|
973
|
-
// 主面板功能开关(v2.2.0 化简:合并行 + 语义增强单独行 + 非净化单独行)
|
|
974
|
-
var featureRows = mergedFeatures.map(function (mf) {
|
|
975
|
-
var allOn = mf.keys.every(function (k) { return f[k] !== false; });
|
|
976
|
-
return el("div", { className: "nwToolRow" + (allOn ? " nwToolRowOn" : ""), key: mf.key },
|
|
977
|
-
el("div", { className: "nwToolLabel" },
|
|
978
|
-
el("div", null, t(mf.labelKey)),
|
|
979
|
-
el("div", { className: "nwToolDesc" }, t(mf.descKey))
|
|
980
|
-
),
|
|
981
|
-
el("button", { type: "button", role: "switch", "aria-checked": allOn ? "true" : "false", className: "nwSwitchSmall" + (allOn ? " nwSwitchSmallOn" : ""), disabled: state.loading,
|
|
982
|
-
onClick: function () {
|
|
983
|
-
var patch = {};
|
|
984
|
-
mf.keys.forEach(function (k) { patch[k] = !allOn; });
|
|
985
|
-
props.toggle({ features: patch });
|
|
986
|
-
} },
|
|
987
|
-
el("span", { className: "nwSwitchSmallKnob" })
|
|
988
|
-
)
|
|
989
|
-
);
|
|
990
|
-
});
|
|
991
|
-
// 语义增强:单独一行 + ⚙ 小模型管理入口(大白话文案)
|
|
992
|
-
featureRows.push(switchRow("semanticEmbedding", f.semanticEmbedding !== false,
|
|
993
|
-
function (n, v) { props.toggle({ features: (function (patch) { patch[n] = v; return patch; })({}) }); },
|
|
994
|
-
el("button", { type: "button", className: "nwModelBtn", title: t("model.manage"), onClick: function (ev) { ev.stopPropagation(); props.controller.openView("model"); } }, "⚙ " + t("model.manage"))
|
|
995
|
-
));
|
|
996
969
|
var TOOL_GROUPS = [
|
|
997
970
|
{ key: "analyze", icon: "📊", tools: ["novel_books", "novel_chapters", "novel_read", "novel_keywords", "novel_sentence_analysis", "novel_style_check", "novel_style_report", "novel_semantic_search", "novel_continuity_check"] },
|
|
998
971
|
{ key: "settings", icon: "📚", tools: ["novel_settings", "novel_plot", "novel_summary", "novel_import"] },
|
|
@@ -1443,10 +1416,7 @@ var TOOL_GROUPS = [
|
|
|
1443
1416
|
entry(t("panel.creationTitle"), t("panel.creationHint"), null, "creation"),
|
|
1444
1417
|
// v3.0.0:风格基线(六维 ±% 容差带)
|
|
1445
1418
|
entry(t("panel.baselineTitle"), t("panel.baselineHint"), null, "baseline"),
|
|
1446
|
-
// v3.7.0
|
|
1447
|
-
el("div", { className: "nwSectionTitle" }, t("panel.featuresTitle")),
|
|
1448
|
-
el("div", { className: "nwToolsHint" }, t("panel.featuresHint")),
|
|
1449
|
-
featureRows,
|
|
1419
|
+
// v3.7.0:功能开关(情感深度/风格检测/语义增强)在二级「功能开关」页维护——主页不重复展示
|
|
1450
1420
|
// v2.6.0:数据目录占用 + 语义引擎状态(一目了然)
|
|
1451
1421
|
el("div", { className: "nwRow nwRowOff" },
|
|
1452
1422
|
el("div", { className: "nwRowText" },
|
package/lib/index.js
CHANGED
|
@@ -1841,7 +1841,9 @@ function registerNovelStyleCheck(ctx, config) {
|
|
|
1841
1841
|
advice: { type: "string" },
|
|
1842
1842
|
semantic: { type: "object", additionalProperties: true },
|
|
1843
1843
|
// v3.0.0:文笔六维基线对照(enabled=false 时仅 note)
|
|
1844
|
-
metric: { type: "object", additionalProperties: true }
|
|
1844
|
+
metric: { type: "object", additionalProperties: true },
|
|
1845
|
+
// v3.8.0:跑偏时附原著锚段(对照修正用)
|
|
1846
|
+
fixAnchors: { type: "array", items: { type: "object", additionalProperties: false, properties: { label: { type: "string" }, text: { type: "string" } }, required: ["label", "text"] } }
|
|
1845
1847
|
},
|
|
1846
1848
|
required: ["book", "chapter", "baselineScope", "similarity", "verdict", "diffs", "chapterFingerprint", "baselineFingerprint", "advice", "semantic"]
|
|
1847
1849
|
},
|
|
@@ -1903,6 +1905,12 @@ function registerNovelStyleCheck(ctx, config) {
|
|
|
1903
1905
|
const baseline = (await bookAnalysisCached(root, book + "__base_" + target.file, baselineText, null)).analysis;
|
|
1904
1906
|
const chapter = analyzeText(targetText, {});
|
|
1905
1907
|
const similarity = fingerprintSimilarity(baseline, chapter);
|
|
1908
|
+
// v3.8.0:偏差时附原著锚段(对照修正用,不整章重写)
|
|
1909
|
+
let fixAnchors = [];
|
|
1910
|
+
try {
|
|
1911
|
+
const pkgA = await buildStyleAnchorPackage(root, book, others, exec, baselineChapters); // 复用已读基线章文本
|
|
1912
|
+
fixAnchors = pkgA.anchors.slice(0, 2);
|
|
1913
|
+
} catch { /* 锚段失败不影响判定 */ }
|
|
1906
1914
|
const diffs = styleDiffs(baseline, chapter);
|
|
1907
1915
|
const verdict = similarity >= 0.9 ? "high" : similarity >= 0.75 ? "medium" : "low";
|
|
1908
1916
|
let advice = "";
|
|
@@ -1911,7 +1919,8 @@ function registerNovelStyleCheck(ctx, config) {
|
|
|
1911
1919
|
} else {
|
|
1912
1920
|
// v3.7.0 ⑦:方向全映射(原只认"偏高","偏长/偏低/偏短"一律输出"略少")
|
|
1913
1921
|
const noteMap = { "偏高": "略多", "偏低": "略少", "偏长": "略长", "偏短": "略短" };
|
|
1914
|
-
|
|
1922
|
+
// v3.8.0:跑偏不整章重写——对照 fixAnchors(原著锚段)逐句修正跑偏维度
|
|
1923
|
+
advice = "续写风格偏差:" + diffs.slice(0, 4).map((d) => d.dimension + (noteMap[d.note] || d.note)).join("、") + "。请对照 fixAnchors 中的原著锚段,逐句对比本章对应写法,只修改跑偏的部分(句式/修饰/节奏),不要整章重写;若为情节需要(如章节情绪转折),说明原因后可接受,但不要持续漂移。";
|
|
1915
1924
|
}
|
|
1916
1925
|
// v2.0.0:语义级风格对比(本地 embedding,可选增强;开关开且模型可用时生效)
|
|
1917
1926
|
let semantic = null;
|
|
@@ -1985,7 +1994,7 @@ function registerNovelStyleCheck(ctx, config) {
|
|
|
1985
1994
|
metric = { enabled: true, baseline: b.baseline, metrics: targetMetrics, verdicts: judge.verdicts, outCount: judge.outCount, summary: judge.summary };
|
|
1986
1995
|
} else metric = { enabled: false, note: "基线章节不足(至少需要 1 个其他章节)" };
|
|
1987
1996
|
} catch (e) { metric = { enabled: false, note: "六维测量失败:" + String(e).slice(0, 80) }; }
|
|
1988
|
-
return {
|
|
1997
|
+
return cleanOutput({
|
|
1989
1998
|
book,
|
|
1990
1999
|
chapter: target.file,
|
|
1991
2000
|
baselineScope: others.length === 1 ? others[0].file : `全书除本章外的 ${others.length} 章`,
|
|
@@ -1996,8 +2005,9 @@ function registerNovelStyleCheck(ctx, config) {
|
|
|
1996
2005
|
metric,
|
|
1997
2006
|
chapterFingerprint: chapter.fingerprint,
|
|
1998
2007
|
baselineFingerprint: baseline.fingerprint,
|
|
2008
|
+
fixAnchors,
|
|
1999
2009
|
advice
|
|
2000
|
-
};
|
|
2010
|
+
});
|
|
2001
2011
|
}
|
|
2002
2012
|
});
|
|
2003
2013
|
}
|
|
@@ -2648,6 +2658,8 @@ function registerNovelStyleReport(ctx, config) {
|
|
|
2648
2658
|
}
|
|
2649
2659
|
}
|
|
2650
2660
|
const { text, chapters } = await readBookAllText(root, book, exec);
|
|
2661
|
+
// v3.8.0:风格锚包(原著代表性段落 + 句式骨架——生成端照样例写,勿只依赖数字基线)
|
|
2662
|
+
const anchorPkg = await buildStyleAnchorPackage(root, book, chapters, exec, text); // 复用 readBookAllText 已读全文
|
|
2651
2663
|
const featState = await readSentenceState();
|
|
2652
2664
|
// v2.6.0:复用全书分析缓存(大书不再每次重算 analyzeText/detectCulture/流派题材)
|
|
2653
2665
|
const { analysis, detection } = await bookAnalysisCached(root, book, text, featState);
|
|
@@ -2745,6 +2757,7 @@ function registerNovelStyleReport(ctx, config) {
|
|
|
2745
2757
|
} else lines.push(" (测量失败)");
|
|
2746
2758
|
lines.push("");
|
|
2747
2759
|
lines.push("── 以上全部为插件测量数据,不包含任何风格判断 ──");
|
|
2760
|
+
lines.push("【风格锚包】anchors/skeletons 字段含原著代表性段落与句式骨架——续写时照样例写,数字基线只做事后校验。");
|
|
2748
2761
|
lines.push("请结合六个维度判断本书的风格气质、读者感受与写法特征。");
|
|
2749
2762
|
const reportText = lines.join("\n");
|
|
2750
2763
|
const saved = {};
|
|
@@ -2780,7 +2793,7 @@ function registerNovelStyleReport(ctx, config) {
|
|
|
2780
2793
|
const dims = Object.keys(result.baseline).filter(function (k) { return result.baseline[k] && typeof result.baseline[k].mu === "number"; });
|
|
2781
2794
|
result.brief = "全书 " + result.chars + " 字:六维基线 μ=" + dims.map(function (k) { return k + ":" + result.baseline[k].mu.toFixed(1); }).join(" ") + ";推荐容差 " + dims.map(function (k) { return result.baseline[k].recTol + "%"; }).slice(0, 3).join("/") + "…。";
|
|
2782
2795
|
}
|
|
2783
|
-
return {
|
|
2796
|
+
return cleanOutput({
|
|
2784
2797
|
book,
|
|
2785
2798
|
chars: result.chars,
|
|
2786
2799
|
brief: result.brief ?? void 0,
|
|
@@ -2791,12 +2804,68 @@ function registerNovelStyleReport(ctx, config) {
|
|
|
2791
2804
|
dimensions: { fingerprint: analysis.fingerprint, culture: result.culture, genre: result.genre?.dominant, theme: result.theme?.dominant, emotion: emotion.cleanDominant, vibe: Object.fromEntries(vibe.axes.slice(0, 12).map((a) => [a.name, a.score])) },
|
|
2792
2805
|
semantic: semantic.map((s, i) => ({ ...s, discrim: i === 0 && semantic.length >= 2 ? Math.round((semantic[0].score - semantic[1].score) * 1000) / 1000 : null })),
|
|
2793
2806
|
...(judgment ? { saved: { savedAt: saved.savedAt, ...(saved.error ? { error: saved.error } : {}) }, savedJudgment: judgment } : {}),
|
|
2807
|
+
anchors: anchorPkg.anchors,
|
|
2808
|
+
skeletons: anchorPkg.skeletons,
|
|
2794
2809
|
message: judgment ? "风格画像报告已生成,判断结果已保存" : "风格画像报告已生成(仅测量数据,判断请由 AI 完成并回传保存)"
|
|
2795
|
-
};
|
|
2810
|
+
});
|
|
2796
2811
|
}
|
|
2797
2812
|
});
|
|
2798
2813
|
}
|
|
2799
2814
|
|
|
2815
|
+
/** v3.8.0:风格锚包——从原著抽取代表性段落(anchors)与句式骨架(skeletons)。
|
|
2816
|
+
* 理念:生成端只喂原文样例(LLM 擅模仿不擅服从数字),统计基线只做校验。纯规则实现,无 embedding 依赖,成本≈0。 */
|
|
2817
|
+
async function buildStyleAnchorPackage(root, book, chapters, exec, preloadedText) {
|
|
2818
|
+
const anchors = [];
|
|
2819
|
+
const skeletons = [];
|
|
2820
|
+
try {
|
|
2821
|
+
// v3.8.0 性能:复用调用方已读文本(preloadedText=string 全文 / {file,text}[] 数组)——大书不再双读
|
|
2822
|
+
let allText = "";
|
|
2823
|
+
if (typeof preloadedText === "string" && preloadedText.length > 0) {
|
|
2824
|
+
allText = preloadedText;
|
|
2825
|
+
} else if (Array.isArray(preloadedText)) {
|
|
2826
|
+
allText = preloadedText.map(function (x) { return x.text || ""; }).join("\n");
|
|
2827
|
+
} else {
|
|
2828
|
+
const dir = bookDir(root, book);
|
|
2829
|
+
for (const ch of chapters) allText += (await readTextFile(join(dir, ch.file), exec)) + "\n";
|
|
2830
|
+
}
|
|
2831
|
+
const paras = allText.split(/\n+/).map(function (p) { return p.trim(); }).filter(function (p) { return p.length >= 40; });
|
|
2832
|
+
const pick = function (list, label, n) {
|
|
2833
|
+
let got = 0;
|
|
2834
|
+
for (const p of list) {
|
|
2835
|
+
if (got >= n) break;
|
|
2836
|
+
anchors.push({ label: label, text: p.slice(0, 200) });
|
|
2837
|
+
got += 1;
|
|
2838
|
+
}
|
|
2839
|
+
};
|
|
2840
|
+
const dialogue = paras.filter(function (p) { return /["“]/.test(p) && p.length <= 200; });
|
|
2841
|
+
const psych = paras.filter(function (p) { return /想|觉得|心|怕|慌|记忆/.test(p) && !/["“]/.test(p) && p.length <= 200; });
|
|
2842
|
+
const desc = paras.filter(function (p) { return !/["“]/.test(p) && p.length >= 80 && p.length <= 200; });
|
|
2843
|
+
pick(dialogue, "对话", 2);
|
|
2844
|
+
pick(psych, "心理", 1);
|
|
2845
|
+
pick(desc, "描写", 2);
|
|
2846
|
+
if (anchors.length === 0) {
|
|
2847
|
+
for (const p of paras.slice(0, 3)) anchors.push({ label: "原文", text: p.slice(0, 200) });
|
|
2848
|
+
}
|
|
2849
|
+
const sentences = allText.split(/[。!?…]+/).map(function (s) { return s.replace(/\s+/g, "").trim(); }).filter(function (s) { return s.length >= 20 && s.length <= 45; });
|
|
2850
|
+
const seen = new Set();
|
|
2851
|
+
for (const s of sentences) {
|
|
2852
|
+
if (skeletons.length >= 4) break;
|
|
2853
|
+
if (seen.has(s)) continue;
|
|
2854
|
+
seen.add(s);
|
|
2855
|
+
let type = "陈述";
|
|
2856
|
+
if (/["“]/.test(s)) type = "对话";
|
|
2857
|
+
else if (/[??]$/.test(s)) type = "疑问";
|
|
2858
|
+
else if (/想|觉得|仿佛|好像|似乎/.test(s)) type = "心理";
|
|
2859
|
+
skeletons.push({ type: type, text: s });
|
|
2860
|
+
}
|
|
2861
|
+
if (skeletons.length === 0) {
|
|
2862
|
+
const firsts = allText.split(/\n+/).map(function (p) { return p.trim(); }).filter(function (p) { return p.length >= 15; }).slice(0, 4);
|
|
2863
|
+
for (const ft of firsts) skeletons.push({ type: "陈述", text: ft.slice(0, 60) });
|
|
2864
|
+
}
|
|
2865
|
+
} catch { /* 锚包失败不影响主功能 */ }
|
|
2866
|
+
return { anchors: anchors.slice(0, 4), skeletons: skeletons.slice(0, 4) };
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2800
2869
|
function parseEvidenceCounts(arr) {
|
|
2801
2870
|
let n = 0;
|
|
2802
2871
|
for (const item of arr || []) {
|
|
@@ -4279,7 +4348,13 @@ function registerNovelNewChapter(ctx, config) {
|
|
|
4279
4348
|
number: { type: "integer" },
|
|
4280
4349
|
file: { type: "string" },
|
|
4281
4350
|
path: { type: "string" },
|
|
4282
|
-
chars: { type: "integer" }
|
|
4351
|
+
chars: { type: "integer" },
|
|
4352
|
+
// v3.7.0:强制风格基线——创建章节时自动附全书六维基线摘要(模型动笔前必得数据)
|
|
4353
|
+
baseline: { type: "string" },
|
|
4354
|
+
reminder: { type: "string" },
|
|
4355
|
+
// v3.8.0:风格锚包——原著代表性段落 + 句式骨架(照样例写,数字只做校验)
|
|
4356
|
+
anchors: { type: "array", items: { type: "object", additionalProperties: false, properties: { label: { type: "string" }, text: { type: "string" } }, required: ["label", "text"] } },
|
|
4357
|
+
skeletons: { type: "array", items: { type: "object", additionalProperties: false, properties: { type: { type: "string" }, text: { type: "string" } }, required: ["type", "text"] } }
|
|
4283
4358
|
},
|
|
4284
4359
|
required: ["book", "number", "file", "path", "chars"]
|
|
4285
4360
|
},
|
|
@@ -4323,13 +4398,49 @@ Created 第${String(value.number).padStart(2, "0")}章 (${value.chars} chars)
|
|
|
4323
4398
|
if (content !== void 0) parts.push(content, "");
|
|
4324
4399
|
const text = parts.join("\n");
|
|
4325
4400
|
await writeFile(filePath, text, { encoding: "utf8", signal: exec.signal });
|
|
4326
|
-
|
|
4401
|
+
// v3.7.0:强制风格基线——创建章节时自动计算全书六维基线附在返回中(即使模型没先调 novel_style_report,也能拿到基线)
|
|
4402
|
+
let baselineLine = "";
|
|
4403
|
+
let anchorPkg = { anchors: [], skeletons: [] };
|
|
4404
|
+
let bTexts = [];
|
|
4405
|
+
try {
|
|
4406
|
+
const allChs = await scanChapters(dir);
|
|
4407
|
+
for (const ch of allChs) {
|
|
4408
|
+
if (ch.file === fileName) continue;
|
|
4409
|
+
bTexts.push({ file: ch.file, text: await readTextFile(join(dir, ch.file), exec) });
|
|
4410
|
+
}
|
|
4411
|
+
if (bTexts.length > 0) {
|
|
4412
|
+
const perCh = await metricChaptersCached(root, book, bTexts);
|
|
4413
|
+
const bl = computeBaselineFromPerChapter(perCh);
|
|
4414
|
+
baselineLine = METRIC_ORDER.map(function (k) { return METRIC_LABELS[k] + " " + (bl[k] && typeof bl[k].mu === "number" ? bl[k].mu : "-"); }).join(" / ");
|
|
4415
|
+
}
|
|
4416
|
+
} catch { /* 基线计算失败不影响章节创建 */ }
|
|
4417
|
+
try {
|
|
4418
|
+
anchorPkg = await buildStyleAnchorPackage(root, book, chapters, exec, bTexts); // 复用基线计算已读文本
|
|
4419
|
+
} catch { /* 锚包失败不影响创建 */ }
|
|
4420
|
+
const reminder = "本章已创建(第" + String(number).padStart(2, "0") + "章)。【风格基线强制流程】① 动笔前:对照上方 baseline 六维 μ,并按 anchors/skeletons 原著样例的味道与句式骨架写(数字只做校验,样例才是写法);② 写完:必须调用 novel_style_check 对照——verdict 低于 high 或出带时,按返回的 fixAnchors 原著锚段逐句修正跑偏部分,不得整章重写;属情节需要则说明原因。";
|
|
4421
|
+
return cleanOutput({ book, number, file: fileName, path: filePath, chars: text.length, baseline: baselineLine, reminder, anchors: anchorPkg.anchors, skeletons: anchorPkg.skeletons });
|
|
4327
4422
|
}
|
|
4328
4423
|
});
|
|
4329
4424
|
}
|
|
4330
4425
|
|
|
4331
4426
|
|
|
4332
4427
|
|
|
4428
|
+
/** v3.7.0:DSH 宿主要求 lossless JSON——清洗 undefined(删键)与 -0(负零→0)。JSON.stringify(-0) 会变 "0",宿主按 Object.is 校验往返一致性即拒绝。 */
|
|
4429
|
+
function cleanOutput(v) {
|
|
4430
|
+
if (typeof v === "number") return Object.is(v, -0) ? 0 : v;
|
|
4431
|
+
if (Array.isArray(v)) return v.map(function (x) { return cleanOutput(x); });
|
|
4432
|
+
if (v !== null && typeof v === "object") {
|
|
4433
|
+
const outObj = {};
|
|
4434
|
+
for (const k of Object.keys(v)) {
|
|
4435
|
+
const val = v[k];
|
|
4436
|
+
if (val === undefined) continue;
|
|
4437
|
+
outObj[k] = cleanOutput(val);
|
|
4438
|
+
}
|
|
4439
|
+
return outObj;
|
|
4440
|
+
}
|
|
4441
|
+
return v;
|
|
4442
|
+
}
|
|
4443
|
+
|
|
4333
4444
|
function apply(ctx, config = {}) {
|
|
4334
4445
|
registerNovelBooks(ctx, config);
|
|
4335
4446
|
registerNovelChapters(ctx, config);
|
package/lib/prompts.js
CHANGED
|
@@ -21,6 +21,8 @@ const WORKFLOW_TEXT = [
|
|
|
21
21
|
" 分析至少覆盖三方面:剧情脉络(冲突、转折、悬念、伏笔、人物关系与目标)、写作手法(叙事视角、节奏、对话、环境与细节描写、比喻意象、留白)、",
|
|
22
22
|
" 以及用 novel_keywords 提取的高频关键词(作者词汇偏好与意象母题),并说明这些手法/关键词在具体章节中的例证。",
|
|
23
23
|
"3. 续写 / 辅助写作时:先通读最近的章节(保持人物、时间线、情节伏笔、文风与已提炼的关键词一致),再动笔。",
|
|
24
|
+
" 【强制·风格基线】动笔前必须调用 novel_style_report 读取全书六维风格基线(μ/σ/容差带)并记录;novel_new_chapter 创建章节时会自动附基线 μ 摘要——若基线与你从正文感受的文风不符,以基线数据为准。",
|
|
25
|
+
" 【v3.8.0·锚包写作】novel_style_report / novel_new_chapter 返回的 anchors(原著代表性段落)与 skeletons(句式骨架)是风格锚——动笔时照锚段的味道与句式的形状写,数字基线只做事后校验;不要试图把数字翻译成写作规则。",
|
|
24
26
|
" 新章节写入 novels/<书名>/ 文件夹:用 novel_new_chapter 创建章节文件,或直接用 write 工具写文件。",
|
|
25
27
|
"4. 句式模式分析(增强,受 UI 开关控制):分析作品时可调用 novel_sentence_analysis 提取句式分布(陈述/环境/心理/对话/疑问/反问/感叹/祈使/省略留白九类)、",
|
|
26
28
|
" 句式排列规律(转移、高频模板、段首段尾句式、按章节的压缩节奏序列)、句长节奏、情感曲线、风格指纹与节奏建议,用于快速掌握作者的写作习惯与主观情感。",
|
|
@@ -32,7 +34,8 @@ const WORKFLOW_TEXT = [
|
|
|
32
34
|
"5. 导入/整理原稿件时:用 novel_import 扫描存放多本小说稿件的文件夹(src),先以 scan 模式查看分组建议,",
|
|
33
35
|
" 若发现异名同书(如\"旧版/精修版\"实为同一本),用 book 参数强制合并后以 apply 模式导入到 novels/<书名>/ 分类存放。",
|
|
34
36
|
"6. 每次续写前先简述:上一章结尾状态 → 本章目标 → 写作计划,再给出正文。",
|
|
35
|
-
"7. 工具链提示:续写/分析前可用 novel_plot 查看未回收伏笔(open
|
|
37
|
+
"7. 工具链提示:续写/分析前可用 novel_plot 查看未回收伏笔(open 条目,含类型/优先级/提及章节);",
|
|
38
|
+
" 【强制·写完对照】每写完一章必须调用 novel_style_check 对照基线(相似度+偏差+细节密度)——verdict 低于 high 或任一维度出带时,按返回的 fixAnchors(原著锚段)逐句对照修正跑偏部分,不得整章重写;确属情节需要则说明原因;novel_plot scan 自动更新伏笔提及记录;",
|
|
36
39
|
" 世界观一致性:续写前先确认文化基准——novel_settings category=worldview action=detect 自动判断(或人工 add/update),动笔时对照其 bannedWords/recommended 用词,避免中西意象混搭(如欧式背景不写老夫/上香/时辰);novel_continuity_check 会扫描用语冲突候选;",
|
|
37
40
|
" 语用一致性:不只管词,还要管'怎么说话'——对照 worldview 的 speechStyle(title 称谓规范/honorBad 客套禁词/ritualBadPatterns 仪式禁式/tone 语气),人物开口前检查称谓是否欧式(Miss+名)、客套是否避免'提点/承蒙/在下'、宗教仪式是否点烛而非烧香/上X柱香、对话是否口语化不文言;",
|
|
38
41
|
" novel_settings 维护五张设定表(人物/地点/道具/时间线/世界观用语规范),novel_summary 保存每章摘要(长书续写先读摘要再按需细读),novel_continuity_check 输出设定矛盾候选;",
|
package/package.json
CHANGED
|
@@ -27,10 +27,13 @@
|
|
|
27
27
|
- **缓存**:结果自动缓存(返回 `reportFile` / `cache`),章节未变重复分析秒回,`fresh=true` 强制重算;
|
|
28
28
|
- 情感词典已去噪("叹气"不算"气"、"不害怕"不计)、祈使/环境分类已增强。
|
|
29
29
|
|
|
30
|
-
## 风格自检(v0.6.0
|
|
30
|
+
## 风格自检(v0.6.0 新增,v3.7.0 起强制)
|
|
31
31
|
|
|
32
|
-
-
|
|
33
|
-
-
|
|
32
|
+
- **续写前必须读基线**:动笔前调用 `novel_style_report`(完整版,包含每维 μ/σ/容差带;或 brief 拿 μ 摘要)确认本书六维风格基线;`novel_new_chapter` 创建章节时会自动附基线 μ 摘要——若发现与自己从正文中感受到的文风不符,以基线数据为准。
|
|
33
|
+
- **v3.8.0 锚包写作**:report / new_chapter 返回的 `anchors`(原著代表性段落)与 `skeletons`(句式骨架)是风格锚——动笔时**照锚段的味道与句式形状写**,数字基线只做事后校验;不要试图把数字翻译成写作规则(语言概括有损且模型不可执行)。
|
|
34
|
+
- **写完必须对照**:每写完一章调用 `novel_style_check`:本章 vs 全书其余章节 → 相似度 + 偏差清单 + 建议;
|
|
35
|
+
- **强制判定(v3.8.0 对照修正)**:verdict 低于 high,或任一维度出带(metric verdicts 含 out)时,按返回的 `fixAnchors` 原著锚段**逐句对照修正跑偏部分,不得整章重写**;确属情节需要则说明原因;
|
|
36
|
+
- 对照结果应体现在续写说明中(偏差维度 + 是否修正)。
|
|
34
37
|
|
|
35
38
|
## 设定管理(v0.8.0 新增)
|
|
36
39
|
|
|
@@ -58,11 +61,12 @@
|
|
|
58
61
|
## 续写工作流
|
|
59
62
|
|
|
60
63
|
1. 先通读最近的章节(必要时读完全书),保持:人物设定、时间线、情节伏笔、文风、已提炼的关键词一致;
|
|
61
|
-
2.
|
|
62
|
-
3.
|
|
63
|
-
4.
|
|
64
|
-
5.
|
|
65
|
-
6.
|
|
64
|
+
2. **读风格基线(强制)**:调用 `novel_style_report` 获取六维基线(μ/σ/容差带),记录基线值——这是续写文风的硬基准;
|
|
65
|
+
3. `novel_plot list` 查看未回收伏笔;
|
|
66
|
+
4. 动笔前简述:上一章结尾状态 → 本章目标 → 写作计划(计划中说明如何贴合基线六维);
|
|
67
|
+
5. 用 `novel_new_chapter` 创建新章节文件(返回会自动附带全书风格基线 μ 摘要,动笔对照),或直接用 `write` 工具写入;
|
|
68
|
+
6. 正文用中文,保持与原文一致的叙事风格与细节密度;句式分析开启时参考指纹与节奏序列,僵硬时回归自然表达;
|
|
69
|
+
7. **写完强制对照基线**:调用 `novel_style_check`——verdict 低于 high 或任一维度出带(out)必须修正,或说明情节需要;然后 `novel_plot add` 登记新伏笔、`novel_outline hook` 回填本章钩子。
|
|
66
70
|
|
|
67
71
|
## 注意事项
|
|
68
72
|
|