dsh-novel-writer 3.9.0 → 3.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +9 -0
- package/README.md +9 -0
- package/lib/index.js +52 -20
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -90,6 +90,15 @@ After installing, **restart the web app** to activate (host registers 16 tools +
|
|
|
90
90
|
|
|
91
91
|
Under `<library-root>/.novel-writer/`: `plots` / `settings` / `summaries` / `analysis` / `audits` / `embedding` / `style-reports`.
|
|
92
92
|
|
|
93
|
+
## Changelog
|
|
94
|
+
|
|
95
|
+
### v3.9.1 (2026-09-05)
|
|
96
|
+
|
|
97
|
+
- 🐛 Fix: per-chapter style analysis (semantic implicit emotion) previously searched the whole book and leaked hits from other chapters into the report; it is now scoped to the chapter, and stale out-of-scope caches are recomputed and written back automatically.
|
|
98
|
+
- 🐛 Fix: `brief=true` previously still returned the full report and anchor payload (token saving never worked); it now returns a single compact brief and omits `dimensions` / `semantic` / anchors / skeletons.
|
|
99
|
+
- 🔧 Tweak: the brief now follows `METRIC_ORDER` with Chinese dimension labels, consistent with the full report.
|
|
100
|
+
- 🧹 Chore: removed the BOM and normalized formatting in `package.json`; `description` bumped to v3.9.1.
|
|
101
|
+
|
|
93
102
|
---
|
|
94
103
|
|
|
95
104
|
## License
|
package/README.md
CHANGED
|
@@ -90,6 +90,15 @@ dsh plugin --profile web add github:siweina/dsh-novel-writer#main
|
|
|
90
90
|
|
|
91
91
|
`<书库根>/.novel-writer/`:`plots`(伏笔)/ `settings`(设定)/ `summaries`(摘要)/ `analysis`(分析报告)/ `audits`(审计)/ `embedding`(语义索引)/ `style-reports`(风格画像)。
|
|
92
92
|
|
|
93
|
+
## 更新日志
|
|
94
|
+
|
|
95
|
+
### v3.9.1(2026-09-05)
|
|
96
|
+
|
|
97
|
+
- 🐛 修复:单章风格分析(语义隐性情感)此前按整书检索,会把其他章节的意象命中混入本章报告;现改为按章节范围检测与检索,越界的旧缓存会自动重算并写回,不再重复触发。
|
|
98
|
+
- 🐛 修复:`brief=true` 精简模式此前仍返回完整报告与锚包,未达到省 token 的目的;现仅返回一行精简简报,并省略六维 `dimensions` / `semantic` / 锚点包等冗余字段。
|
|
99
|
+
- 🔧 优化:简报按 `METRIC_ORDER` 顺序输出并使用中文维度标签,与完整报告口径一致。
|
|
100
|
+
- 🧹 整理:`package.json` 去除 BOM、恢复规范排版,`description` 更新至 v3.9.1。
|
|
101
|
+
|
|
93
102
|
---
|
|
94
103
|
|
|
95
104
|
## 许可证
|
package/lib/index.js
CHANGED
|
@@ -930,17 +930,29 @@ function featureEnabled(state, name) {
|
|
|
930
930
|
return features[name] !== false;
|
|
931
931
|
}
|
|
932
932
|
|
|
933
|
-
/** v2.0.0 语义隐性情感增强:规则意象表之外,用情感原型句扫全书索引找"词表外疑似意象段落"。
|
|
934
|
-
|
|
933
|
+
/** v2.0.0 语义隐性情感增强:规则意象表之外,用情感原型句扫全书索引找"词表外疑似意象段落"。
|
|
934
|
+
* v3.9.1 修复:scope=章节文件名时只在章节范围内检测/复用——此前整书检索会把别章命中混入本章报告,
|
|
935
|
+
* 且单章缓存命中时旧版越界结果会被直接复用(早退),须校验 hits 的 chapter 全部落在 scope 内才早退。 */
|
|
936
|
+
async function enrichSemanticImplicit(state, root, book, result, exec, scope) {
|
|
935
937
|
try {
|
|
936
938
|
if (!semanticFeatureEnabled(state, "semanticImplicit")) return result;
|
|
937
939
|
if (!(await embedding.isAvailable())) return result;
|
|
938
940
|
// v3.9.0 修正:缓存已带 semanticImplicit(miss 路径算好写盘的)→ 直接复用,不再重建索引全量读盘
|
|
939
|
-
|
|
941
|
+
const existed = result?.emotion?.quantification?.semanticImplicit;
|
|
942
|
+
if (existed) {
|
|
943
|
+
const hits = Array.isArray(existed.hits) ? existed.hits : [];
|
|
944
|
+
// v3.9.1 范围校验:单章分析时命中必须全部落在本章;全书分析(scope=undefined)不做约束
|
|
945
|
+
const inScope = scope === void 0 || hits.length === 0 || hits.every((h) => h.chapter === scope);
|
|
946
|
+
if (inScope) return result;
|
|
947
|
+
delete result.emotion.quantification.semanticImplicit; // 越界旧缓存 → 重算修正
|
|
948
|
+
}
|
|
940
949
|
const dir = bookDir(root, book);
|
|
941
950
|
const chapters = await scanChapters(dir);
|
|
951
|
+
// v3.9.1:单章范围只读该章文本(避免整书读盘与检索越界);全书照旧
|
|
952
|
+
const targets = scope === void 0 ? chapters : chapters.filter((c) => c.file === scope);
|
|
953
|
+
if (targets.length === 0) return result;
|
|
942
954
|
const chunks = [];
|
|
943
|
-
for (const chapter of
|
|
955
|
+
for (const chapter of targets) {
|
|
944
956
|
const text = await readTextFile(join(dir, chapter.file), exec);
|
|
945
957
|
// v2.6.0:章节标记直接用文件名(chunkText 只认"第X章"标题行,无标题行时检索结果无法定位章节)
|
|
946
958
|
for (const p of embedding.chunkText(text)) chunks.push({ ...p, id: chapter.file + "|" + p.id, chapter: chapter.file });
|
|
@@ -948,11 +960,18 @@ async function enrichSemanticImplicit(state, root, book, result, exec) {
|
|
|
948
960
|
// v2.5.0 修复轮 4:内容指纹失效重建——章节更新后旧缓存不再命中(不靠版本号)
|
|
949
961
|
// v2.6.0:增量构建——指纹变化时只对新/变化的段落做推理,其余复用旧向量
|
|
950
962
|
const { items: cachedIndex, fp: cachedFp } = embedding.loadIndexMeta(root, book);
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
embedding.
|
|
963
|
+
let index;
|
|
964
|
+
if (scope !== void 0) {
|
|
965
|
+
// v3.9.1 单章范围:复用全书缓存中本章向量(增量只补缺失段),不写盘——避免污染全书指纹缓存
|
|
966
|
+
const cachedForScope = (cachedIndex || []).filter((it) => it.chapter === scope);
|
|
967
|
+
index = await embedding.buildIndexIncremental(chunks, cachedForScope);
|
|
968
|
+
} else {
|
|
969
|
+
const indexFp = embedding.fingerprint(chunks);
|
|
970
|
+
index = cachedIndex;
|
|
971
|
+
if (!index || index.length === 0 || cachedFp !== indexFp) {
|
|
972
|
+
index = await embedding.buildIndexIncremental(chunks, cachedIndex);
|
|
973
|
+
embedding.saveIndex(root, book, index, indexFp);
|
|
974
|
+
}
|
|
956
975
|
}
|
|
957
976
|
const implicit = await embedding.detectImplicitEmotions(index);
|
|
958
977
|
if (result.emotion?.quantification) {
|
|
@@ -1783,7 +1802,14 @@ function registerNovelSentenceAnalysis(ctx, config) {
|
|
|
1783
1802
|
}
|
|
1784
1803
|
if (cachedHit) {
|
|
1785
1804
|
// v3.9.0 复查修正:先 enrich(缓存自带 semanticImplicit → 早退零读盘)再裁剪——emotionComplexity 关闭时也不会触发全量重读
|
|
1786
|
-
|
|
1805
|
+
// v3.9.1 修复:单章分析传 scope,语义隐性情感只算本章——旧版越界缓存(全书记录)命中后会自动重算修正
|
|
1806
|
+
const siBefore = cachedHit?.emotion?.quantification?.semanticImplicit;
|
|
1807
|
+
await enrichSemanticImplicit(state, root, book, cachedHit, exec, chapterArg !== void 0 ? selected[0].file : void 0);
|
|
1808
|
+
// v3.9.1 修复:越界旧缓存被重算(对象被替换)→ 写回缓存,避免每次命中重复重算
|
|
1809
|
+
const siAfter = cachedHit?.emotion?.quantification?.semanticImplicit;
|
|
1810
|
+
if (siBefore && siAfter && siBefore !== siAfter) {
|
|
1811
|
+
try { await atomicWriteJson(reportFile, { ...cachedHit, cachedAt: new Date().toISOString(), ver: CACHE_VERSION }); } catch { /* 缓存修正写回失败不阻塞 */ }
|
|
1812
|
+
}
|
|
1787
1813
|
if (!featureEnabled(state, "emotionCaveat") && cachedHit.emotion) {
|
|
1788
1814
|
cachedHit.emotion = cropEmotion(cachedHit.emotion);
|
|
1789
1815
|
}
|
|
@@ -1824,7 +1850,8 @@ function registerNovelSentenceAnalysis(ctx, config) {
|
|
|
1824
1850
|
}
|
|
1825
1851
|
} catch { /* 语义裁决失败不阻塞 */ }
|
|
1826
1852
|
// v3.9.0 复查修正:先 enrich 后写缓存——缓存文件必须含 semanticImplicit,命中路径才能早退复用(否则每次命中仍全量读盘重建索引)
|
|
1827
|
-
|
|
1853
|
+
// v3.9.1 修复:单章分析传 scope,语义隐性情感只算本章(此前整书检索,把别章命中混入本章报告)
|
|
1854
|
+
await enrichSemanticImplicit(state, root, book, result, exec, chapterArg !== void 0 ? selected[0].file : void 0);
|
|
1828
1855
|
try {
|
|
1829
1856
|
await mkdir(cacheDir, { recursive: true });
|
|
1830
1857
|
await atomicWriteJson(reportFile, { ...result, cachedAt: new Date().toISOString(), ver: CACHE_VERSION });
|
|
@@ -3028,11 +3055,17 @@ function registerNovelStyleReport(ctx, config) {
|
|
|
3028
3055
|
const promptLine = judgment
|
|
3029
3056
|
? "\n✅ 风格判断已保存:" + (saved.savedAt || "(存盘失败)")
|
|
3030
3057
|
: "\n📌 请结合上述六个维度数据判断本书的风格气质、读者感受与写法特征,";
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3058
|
+
// v3.9.1 修复:brief=true 时真正精简输出(此前仅附加 brief 字段、report 仍全文+锚包,未达省 token 目的)
|
|
3059
|
+
const briefMode = args?.brief === true;
|
|
3060
|
+
if (briefMode && result.baseline) {
|
|
3061
|
+
// v3.9.1 优化:按 METRIC_ORDER 顺序输出、用中文标签(与完整报告一致)
|
|
3062
|
+
const dims = METRIC_ORDER.filter(function (k) { return result.baseline[k] && typeof result.baseline[k].mu === "number"; });
|
|
3063
|
+
result.brief = "全书 " + result.chars + " 字:六维基线 μ=" + dims.map(function (k) { return METRIC_LABELS[k] + ":" + result.baseline[k].mu.toFixed(1); }).join(" ") + ";推荐容差 " + dims.map(function (k) { return result.baseline[k].recTol + "%"; }).slice(0, 3).join("/") + "…。";
|
|
3034
3064
|
}
|
|
3035
3065
|
if (judgment) await noteRoot(root); // v3.9.0:判断保存记录书库根
|
|
3066
|
+
const briefReport = briefMode && result.brief
|
|
3067
|
+
? "【风格画像·精简】《" + book + "》\n" + result.brief + promptLine + "\n(brief=true:完整六维/氛围/锚包请省略 brief 重新调用)"
|
|
3068
|
+
: "";
|
|
3036
3069
|
return cleanOutput({
|
|
3037
3070
|
book,
|
|
3038
3071
|
chars: result.chars,
|
|
@@ -3040,13 +3073,12 @@ function registerNovelStyleReport(ctx, config) {
|
|
|
3040
3073
|
// v3.0.0:文笔六维基线带(novel_style_check 对照用)
|
|
3041
3074
|
baseline: result.baseline ?? null,
|
|
3042
3075
|
baselineChapterCount: result.baselineChapterCount ?? 0,
|
|
3043
|
-
report: reportText + promptLine + (judgment ? "" : "然后将判断结论通过 aiJudgment 参数回传,插件会存入 .novel-writer/style-reports/ 供后续续写参考。"),
|
|
3044
|
-
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])) },
|
|
3045
|
-
semantic: semantic.map((s, i) => ({ ...s, discrim: i === 0 && semantic.length >= 2 ? Math.round((semantic[0].score - semantic[1].score) * 1000) / 1000 : null })),
|
|
3076
|
+
report: briefReport !== "" ? briefReport : reportText + promptLine + (judgment ? "" : "然后将判断结论通过 aiJudgment 参数回传,插件会存入 .novel-writer/style-reports/ 供后续续写参考。"),
|
|
3077
|
+
dimensions: briefMode ? void 0 : { 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])) },
|
|
3078
|
+
semantic: briefMode ? void 0 : semantic.map((s, i) => ({ ...s, discrim: i === 0 && semantic.length >= 2 ? Math.round((semantic[0].score - semantic[1].score) * 1000) / 1000 : null })),
|
|
3046
3079
|
...(judgment ? { saved: { savedAt: saved.savedAt, ...(saved.error ? { error: saved.error } : {}) }, savedJudgment: judgment } : {}),
|
|
3047
|
-
anchors: anchorPkg.anchors,
|
|
3048
|
-
|
|
3049
|
-
message: judgment ? (saved?.error ? "风格画像报告已生成,但判断保存失败:" + saved.error : "风格画像报告已生成,判断结果已保存") : "风格画像报告已生成(仅测量数据,判断请由 AI 完成并回传保存)"
|
|
3080
|
+
...(briefMode ? {} : { anchors: anchorPkg.anchors, skeletons: anchorPkg.skeletons }),
|
|
3081
|
+
message: judgment ? (saved?.error ? "风格画像报告已生成,但判断保存失败:" + saved.error : "风格画像报告已生成,判断结果已保存") : (briefMode ? "风格画像简报已生成(精简)" : "风格画像报告已生成(仅测量数据,判断请由 AI 完成并回传保存)")
|
|
3050
3082
|
});
|
|
3051
3083
|
}
|
|
3052
3084
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-novel-writer",
|
|
3
|
-
"version": "3.9.
|
|
4
|
-
"description": "小说写作助手插件(v3.9.
|
|
3
|
+
"version": "3.9.1",
|
|
4
|
+
"description": "小说写作助手插件(v3.9.1):句式/情感/意象分析、伏笔设定管理、本地语义检索、氛围光谱、风格画像报告、文笔六维基线带、原创模式与创作资料、写作哨兵(衔接/OOC/大纲)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|