dsh-novel-writer 3.7.0 → 3.7.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/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 ②:主面板功能开关(合并行:情感深度/风格检测/语义增强 + 非净化)——原注释位置,此前误渲染进 tools 子页
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
@@ -1985,7 +1985,7 @@ function registerNovelStyleCheck(ctx, config) {
1985
1985
  metric = { enabled: true, baseline: b.baseline, metrics: targetMetrics, verdicts: judge.verdicts, outCount: judge.outCount, summary: judge.summary };
1986
1986
  } else metric = { enabled: false, note: "基线章节不足(至少需要 1 个其他章节)" };
1987
1987
  } catch (e) { metric = { enabled: false, note: "六维测量失败:" + String(e).slice(0, 80) }; }
1988
- return {
1988
+ return cleanOutput({
1989
1989
  book,
1990
1990
  chapter: target.file,
1991
1991
  baselineScope: others.length === 1 ? others[0].file : `全书除本章外的 ${others.length} 章`,
@@ -1997,7 +1997,7 @@ function registerNovelStyleCheck(ctx, config) {
1997
1997
  chapterFingerprint: chapter.fingerprint,
1998
1998
  baselineFingerprint: baseline.fingerprint,
1999
1999
  advice
2000
- };
2000
+ });
2001
2001
  }
2002
2002
  });
2003
2003
  }
@@ -2780,7 +2780,7 @@ function registerNovelStyleReport(ctx, config) {
2780
2780
  const dims = Object.keys(result.baseline).filter(function (k) { return result.baseline[k] && typeof result.baseline[k].mu === "number"; });
2781
2781
  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
2782
  }
2783
- return {
2783
+ return cleanOutput({
2784
2784
  book,
2785
2785
  chars: result.chars,
2786
2786
  brief: result.brief ?? void 0,
@@ -2792,7 +2792,7 @@ function registerNovelStyleReport(ctx, config) {
2792
2792
  semantic: semantic.map((s, i) => ({ ...s, discrim: i === 0 && semantic.length >= 2 ? Math.round((semantic[0].score - semantic[1].score) * 1000) / 1000 : null })),
2793
2793
  ...(judgment ? { saved: { savedAt: saved.savedAt, ...(saved.error ? { error: saved.error } : {}) }, savedJudgment: judgment } : {}),
2794
2794
  message: judgment ? "风格画像报告已生成,判断结果已保存" : "风格画像报告已生成(仅测量数据,判断请由 AI 完成并回传保存)"
2795
- };
2795
+ });
2796
2796
  }
2797
2797
  });
2798
2798
  }
@@ -4279,7 +4279,10 @@ function registerNovelNewChapter(ctx, config) {
4279
4279
  number: { type: "integer" },
4280
4280
  file: { type: "string" },
4281
4281
  path: { type: "string" },
4282
- chars: { type: "integer" }
4282
+ chars: { type: "integer" },
4283
+ // v3.7.0:强制风格基线——创建章节时自动附全书六维基线摘要(模型动笔前必得数据)
4284
+ baseline: { type: "string" },
4285
+ reminder: { type: "string" }
4283
4286
  },
4284
4287
  required: ["book", "number", "file", "path", "chars"]
4285
4288
  },
@@ -4323,13 +4326,45 @@ Created 第${String(value.number).padStart(2, "0")}章 (${value.chars} chars)
4323
4326
  if (content !== void 0) parts.push(content, "");
4324
4327
  const text = parts.join("\n");
4325
4328
  await writeFile(filePath, text, { encoding: "utf8", signal: exec.signal });
4326
- return { book, number, file: fileName, path: filePath, chars: text.length };
4329
+ // v3.7.0:强制风格基线——创建章节时自动计算全书六维基线附在返回中(即使模型没先调 novel_style_report,也能拿到基线)
4330
+ let baselineLine = "";
4331
+ try {
4332
+ const allChs = await scanChapters(dir);
4333
+ const bTexts = [];
4334
+ for (const ch of allChs) {
4335
+ if (ch.file === fileName) continue;
4336
+ bTexts.push({ file: ch.file, text: await readTextFile(join(dir, ch.file), exec) });
4337
+ }
4338
+ if (bTexts.length > 0) {
4339
+ const perCh = await metricChaptersCached(root, book, bTexts);
4340
+ const bl = computeBaselineFromPerChapter(perCh);
4341
+ baselineLine = METRIC_ORDER.map(function (k) { return METRIC_LABELS[k] + " " + (bl[k] && typeof bl[k].mu === "number" ? bl[k].mu : "-"); }).join(" / ");
4342
+ }
4343
+ } catch { /* 基线计算失败不影响章节创建 */ }
4344
+ const reminder = "本章已创建(第" + String(number).padStart(2, "0") + "章)。【风格基线强制流程】动笔前请对照上方 baseline 六维 μ(完整基线可再调 novel_style_report 获取每维 σ 与容差带);写完本章后必须调用 novel_style_check 对照基线——verdict 低于 high 或任一维度出带,需修正文风或说明情节需要,不得跳过。";
4345
+ return cleanOutput({ book, number, file: fileName, path: filePath, chars: text.length, baseline: baselineLine, reminder });
4327
4346
  }
4328
4347
  });
4329
4348
  }
4330
4349
 
4331
4350
 
4332
4351
 
4352
+ /** v3.7.0:DSH 宿主要求 lossless JSON——清洗 undefined(删键)与 -0(负零→0)。JSON.stringify(-0) 会变 "0",宿主按 Object.is 校验往返一致性即拒绝。 */
4353
+ function cleanOutput(v) {
4354
+ if (typeof v === "number") return Object.is(v, -0) ? 0 : v;
4355
+ if (Array.isArray(v)) return v.map(function (x) { return cleanOutput(x); });
4356
+ if (v !== null && typeof v === "object") {
4357
+ const outObj = {};
4358
+ for (const k of Object.keys(v)) {
4359
+ const val = v[k];
4360
+ if (val === undefined) continue;
4361
+ outObj[k] = cleanOutput(val);
4362
+ }
4363
+ return outObj;
4364
+ }
4365
+ return v;
4366
+ }
4367
+
4333
4368
  function apply(ctx, config = {}) {
4334
4369
  registerNovelBooks(ctx, config);
4335
4370
  registerNovelChapters(ctx, config);
package/lib/prompts.js CHANGED
@@ -21,6 +21,7 @@ const WORKFLOW_TEXT = [
21
21
  " 分析至少覆盖三方面:剧情脉络(冲突、转折、悬念、伏笔、人物关系与目标)、写作手法(叙事视角、节奏、对话、环境与细节描写、比喻意象、留白)、",
22
22
  " 以及用 novel_keywords 提取的高频关键词(作者词汇偏好与意象母题),并说明这些手法/关键词在具体章节中的例证。",
23
23
  "3. 续写 / 辅助写作时:先通读最近的章节(保持人物、时间线、情节伏笔、文风与已提炼的关键词一致),再动笔。",
24
+ " 【强制·风格基线】动笔前必须调用 novel_style_report 读取全书六维风格基线(μ/σ/容差带)并记录;novel_new_chapter 创建章节时会自动附基线 μ 摘要——若基线与你从正文感受的文风不符,以基线数据为准。",
24
25
  " 新章节写入 novels/<书名>/ 文件夹:用 novel_new_chapter 创建章节文件,或直接用 write 工具写文件。",
25
26
  "4. 句式模式分析(增强,受 UI 开关控制):分析作品时可调用 novel_sentence_analysis 提取句式分布(陈述/环境/心理/对话/疑问/反问/感叹/祈使/省略留白九类)、",
26
27
  " 句式排列规律(转移、高频模板、段首段尾句式、按章节的压缩节奏序列)、句长节奏、情感曲线、风格指纹与节奏建议,用于快速掌握作者的写作习惯与主观情感。",
@@ -32,7 +33,8 @@ const WORKFLOW_TEXT = [
32
33
  "5. 导入/整理原稿件时:用 novel_import 扫描存放多本小说稿件的文件夹(src),先以 scan 模式查看分组建议,",
33
34
  " 若发现异名同书(如\"旧版/精修版\"实为同一本),用 book 参数强制合并后以 apply 模式导入到 novels/<书名>/ 分类存放。",
34
35
  "6. 每次续写前先简述:上一章结尾状态 → 本章目标 → 写作计划,再给出正文。",
35
- "7. 工具链提示:续写/分析前可用 novel_plot 查看未回收伏笔(open 条目,含类型/优先级/提及章节);写完新章节可用 novel_style_check 做风格自检(相似度+偏差+细节密度),novel_plot scan 自动更新伏笔提及记录;",
36
+ "7. 工具链提示:续写/分析前可用 novel_plot 查看未回收伏笔(open 条目,含类型/优先级/提及章节);",
37
+ " 【强制·写完对照】每写完一章必须调用 novel_style_check 对照基线(相似度+偏差+细节密度)——verdict 低于 high 或任一维度出带必须修正文风,或说明情节需要;novel_plot scan 自动更新伏笔提及记录;",
36
38
  " 世界观一致性:续写前先确认文化基准——novel_settings category=worldview action=detect 自动判断(或人工 add/update),动笔时对照其 bannedWords/recommended 用词,避免中西意象混搭(如欧式背景不写老夫/上香/时辰);novel_continuity_check 会扫描用语冲突候选;",
37
39
  " 语用一致性:不只管词,还要管'怎么说话'——对照 worldview 的 speechStyle(title 称谓规范/honorBad 客套禁词/ritualBadPatterns 仪式禁式/tone 语气),人物开口前检查称谓是否欧式(Miss+名)、客套是否避免'提点/承蒙/在下'、宗教仪式是否点烛而非烧香/上X柱香、对话是否口语化不文言;",
38
40
  " novel_settings 维护五张设定表(人物/地点/道具/时间线/世界观用语规范),novel_summary 保存每章摘要(长书续写先读摘要再按需细读),novel_continuity_check 输出设定矛盾候选;",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-novel-writer",
3
- "version": "3.7.0",
3
+ "version": "3.7.1",
4
4
  "description": "小说写作助手插件(v3.7.0):句式/情感/意象分析、伏笔设定管理、本地语义检索、氛围光谱、风格画像报告、文笔六维基线带、原创模式与创作资料、写作哨兵(衔接/OOC/大纲)。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -27,10 +27,12 @@
27
27
  - **缓存**:结果自动缓存(返回 `reportFile` / `cache`),章节未变重复分析秒回,`fresh=true` 强制重算;
28
28
  - 情感词典已去噪("叹气"不算"气"、"不害怕"不计)、祈使/环境分类已增强。
29
29
 
30
- ## 风格自检(v0.6.0 新增)
30
+ ## 风格自检(v0.6.0 新增,v3.7.0 起强制)
31
31
 
32
- - 写完新章节后调用 `novel_style_check`:本章 vs 全书其余章节 相似度 + 偏差清单 + 建议;
33
- - 偏差明显时主动调整文风(对话/心理占比、句长、情绪),防止漂移。
32
+ - **续写前必须读基线**:动笔前调用 `novel_style_report`(完整版,包含每维 μ/σ/容差带;或 brief μ 摘要)确认本书六维风格基线;`novel_new_chapter` 创建章节时会自动附基线 μ 摘要——若发现与自己从正文中感受到的文风不符,以基线数据为准。
33
+ - **写完必须对照**:每写完一章调用 `novel_style_check`:本章 vs 全书其余章节 → 相似度 + 偏差清单 + 建议;
34
+ - **强制判定**:verdict 低于 high,或任一维度出带(metric verdicts 含 out),必须修正文风(对话/心理占比、句长、情绪、留白),除非确有情节需要并在回复中说明原因;
35
+ - 对照结果应体现在续写说明中(偏差维度 + 是否修正)。
34
36
 
35
37
  ## 设定管理(v0.8.0 新增)
36
38
 
@@ -58,11 +60,12 @@
58
60
  ## 续写工作流
59
61
 
60
62
  1. 先通读最近的章节(必要时读完全书),保持:人物设定、时间线、情节伏笔、文风、已提炼的关键词一致;
61
- 2. `novel_plot list` 查看未回收伏笔;
62
- 3. 动笔前简述:上一章结尾状态 本章目标 → 写作计划;
63
- 4. `novel_new_chapter` 创建新章节文件,或直接用 `write` 工具写入;
64
- 5. 正文用中文,保持与原文一致的叙事风格与细节密度;句式分析开启时参考指纹与节奏序列,僵硬时回归自然表达;
65
- 6. 写完后 `novel_style_check` 自检文风,`novel_plot add` 登记新伏笔。
63
+ 2. **读风格基线(强制)**:调用 `novel_style_report` 获取六维基线(μ/σ/容差带),记录基线值——这是续写文风的硬基准;
64
+ 3. `novel_plot list` 查看未回收伏笔;
65
+ 4. 动笔前简述:上一章结尾状态 本章目标 写作计划(计划中说明如何贴合基线六维);
66
+ 5. 用 `novel_new_chapter` 创建新章节文件(返回会自动附带全书风格基线 μ 摘要,动笔对照),或直接用 `write` 工具写入;
67
+ 6. 正文用中文,保持与原文一致的叙事风格与细节密度;句式分析开启时参考指纹与节奏序列,僵硬时回归自然表达;
68
+ 7. **写完强制对照基线**:调用 `novel_style_check`——verdict 低于 high 或任一维度出带(out)必须修正,或说明情节需要;然后 `novel_plot add` 登记新伏笔、`novel_outline hook` 回填本章钩子。
66
69
 
67
70
  ## 注意事项
68
71