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 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
- async function enrichSemanticImplicit(state, root, book, result, exec) {
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
- if (result?.emotion?.quantification?.semanticImplicit) return result;
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 chapters) {
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
- const indexFp = embedding.fingerprint(chunks);
952
- let index = cachedIndex;
953
- if (!index || index.length === 0 || cachedFp !== indexFp) {
954
- index = await embedding.buildIndexIncremental(chunks, cachedIndex);
955
- embedding.saveIndex(root, book, index, indexFp);
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
- await enrichSemanticImplicit(state, root, book, cachedHit, exec);
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
- await enrichSemanticImplicit(state, root, book, result, exec);
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
- if (args?.brief === true && result.baseline) {
3032
- const dims = Object.keys(result.baseline).filter(function (k) { return result.baseline[k] && typeof result.baseline[k].mu === "number"; });
3033
- 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("/") + "…。";
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
- skeletons: anchorPkg.skeletons,
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.0",
4
- "description": "小说写作助手插件(v3.9.0):句式/情感/意象分析、伏笔设定管理、本地语义检索、氛围光谱、风格画像报告、文笔六维基线带、原创模式与创作资料、写作哨兵(衔接/OOC/大纲)。",
3
+ "version": "3.9.1",
4
+ "description": "小说写作助手插件(v3.9.1):句式/情感/意象分析、伏笔设定管理、本地语义检索、氛围光谱、风格画像报告、文笔六维基线带、原创模式与创作资料、写作哨兵(衔接/OOC/大纲)。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {