dsh-novel-writer 0.6.0 → 0.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/INSTALL.md CHANGED
@@ -1,11 +1,11 @@
1
- # dsh-novel-writer v0.6.0 — 安装指南
1
+ # dsh-novel-writer v0.7.0 — 安装指南
2
2
 
3
- 基于 v0.5.0 合并版的工程化升级。新增:分析缓存/报告导出、风格自检、伏笔登记表、关键词三字组与疑似人名、情感词典去噪、局域网开关可选放行。
3
+ 基于 v0.6.0 工程化版的 UI 升级:10 个工具全部带独立 UI 开关(侧边栏「写作助手功能」面板统一管理)。
4
4
 
5
5
  ```
6
6
  dsh-novel-writer-v0.6.0/
7
7
  ├── lib/
8
- │ ├── index.js # 宿主端(10 个工具 + state 路由 + 缓存)
8
+ │ ├── index.js # 宿主端(10 个工具全开关 + state 路由 + 缓存)
9
9
  │ ├── analysis.js # 句式模式分析引擎(含 fingerprintSimilarity / styleDiffs)
10
10
  │ └── client.js # 浏览器端(侧边栏面板 + 设置页状态卡片)
11
11
  ├── test/ # 引擎 + client + e2e 测试
package/README.md CHANGED
@@ -1,61 +1,28 @@
1
- # dsh-novel-writer — 小说写作助手插件(v0.6.0 工程化版)
1
+ # dsh-novel-writer — 小说写作助手插件(v0.8.0:记忆层)
2
2
 
3
3
  DSH(DeepSeek Harness)小说写作助手 bundle 插件。宿主端零第三方依赖(仅 Node 内置模块),
4
4
  浏览器端仅依赖 Web GUI 自带的 react。
5
- **v0.6.0 在 v0.5.0(合并版)基础上完成工程化升级**:分析缓存与报告导出、风格自检、伏笔登记表、
5
+ **v0.8.0 在 v0.7.0 基础上补全「记忆层」:四张设定表、章节摘要、连贯性审计、伏笔字段化与提及追踪、细节密度指标、统一数据目录、UI 路径可视化修复**:分析缓存与报告导出、风格自检、伏笔登记表、
6
6
  关键词三字组/疑似人名、情感词典去噪、祈使句与环境分类改进、局域网开关可选放行。
7
7
 
8
- ## 工具清单(10 个)
8
+ ## 工具清单(13 个,全部带独立 UI 开关)
9
9
 
10
- | 工具 | 说明 |
11
- |---|---|
12
- | `novel_books` | 列出章节库全部作品 |
13
- | `novel_chapters` | 列出某作品章节清单 |
14
- | `novel_read` | 阅读某章正文(offset/limit 分段,编码自动探测) |
15
- | `novel_keywords` | 关键词:二字组 + **三字组** + **疑似人名** + 英文词 |
16
- | `novel_new_chapter` | 创建新章节文件 |
17
- | `novel_import` | 原稿件批量导入/自动分类(含**异名同书提示**) |
18
- | `novel_sentence_analysis` | 句式模式分析(九类/情感曲线/章节节奏/指纹,**带缓存与报告文件**) |
19
- | `novel_sentence_config` | 查看/修改句式分析开关 |
20
- | `novel_style_check` | **风格自检(v0.6.0 新增)**:章节 vs 全书指纹对比 + 偏差清单 |
21
- | `novel_plot` | **伏笔/剧情线登记表(v0.6.0 新增)**:list/add/update/done/delete |
10
+ novel_books / novel_chapters / novel_read / novel_keywords / novel_new_chapter / novel_import / novel_sentence_analysis / novel_sentence_config / novel_style_check / novel_plot / **novel_settings(设定管理)** / **novel_summary(章节摘要)** / **novel_continuity_check(连贯性审计)**
22
11
 
23
- ## v0.6.0 新增能力
12
+ ## v0.8.0 新增能力
24
13
 
25
- 1. **分析缓存 + 报告导出**:novel_sentence_analysis 结果自动缓存到 `~/.dsh/dsh-novel-writer/cache/<书名>-<hash>.json`,
26
- 返回值带 `reportFile` / `cache`(hit/miss)字段;章节未改动时重复分析秒回,传 `fresh=true` 强制重算。
27
- 2. **风格自检 novel_style_check**:任意章节 vs 全书其余章节 → 余弦相似度 + 句式/句长/情绪偏差清单 + 续写建议(写完新章自动查一次,防止文风漂移)。
28
- 3. **伏笔登记表 novel_plot**:`novels 根目录/.novel-writer/<书名>.json` 持久化;续写前 list 查看未回收伏笔。
29
- 4. **关键词升级**:新增三字组与疑似人名("XX说/道/问"与"XX小姐/导师"模式),人名不再被拆成"露西+西亚"。
30
- 5. **情感词典去噪**:单字词搭配排除("叹气"不算"气"、"笑道"不算"笑")、否定过滤("不害怕"不计)、副词距离窗口化("非常开心"才加权)。
31
- 6. **分类改进**:祈使句支持"快睡吧,明天还要上班"式(命令动词开头+吧);环境词表扩充(晚风/晨曦/森林/群山…)。
32
- 7. **情感曲线粒度可调**:`curveSegments` 参数(1-50,默认 20)。
33
- 8. **novel_import 异名同书提示**:scan 结果中组名包含/高重合时列出 `maybe` 字段,提示用 `book` 合并。
34
- 9. **局域网开关可选放行**:config `allowLanState: true` 时,state 路由对同源局域网请求放行(默认仍仅 loopback)。
14
+ - **novel_settings(四张表)**:人物卡/地点卡/道具清单/时间线,list/add/update/delete + scan 候选提取;
15
+ - **novel_summary**:模型生成、插件存储的每章摘要(长书续写先读摘要);
16
+ - **novel_continuity_check**:对照设定表扫描全书 → 数字口径/人物缺场/别名/重复条目矛盾候选;
17
+ - **novel_plot 字段化**:type/priority/relatedCharacters/locations/payoffCondition/mentionedIn/lastMentioned + scan 自动提及追踪;
18
+ - **细节密度指标**:动作链密度/物件名词/感官词(千字归一)进 novel_sentence_analysis 与 novel_style_check;
19
+ - **统一数据目录**:§BT§<书库根>/.novel-writer/§BT§ 下 plots/ settings/ summaries/ analysis/ audits/(伏笔旧位置自动迁移;分析缓存与关键词报告落盘到 analysis/);
20
+ - **UI 路径修复**:cordis 空字符串 root 回退 lastRoot、面板刷新按钮、打开/复制按钮不再禁用、显示 dataDir。
35
21
 
36
- ## UI 开关(与 v0.5.0 相同,统一在一处)
22
+ ## v0.7.0 更新内容
37
23
 
38
- - 唯一开关位置:侧边栏「句式分析」面板(总开关 enabled + 子开关 autoAnalyze);
39
- - 设置 > 插件配置 卡片仅显示状态 + 「打开开关面板」跳转;
40
- - 持久化 `~/.dsh/dsh-novel-writer/state.json`;兼容 v0.4.0 的 novel-writer.json / config.stylePattern;
41
- - 默认开启;路由 `/api/dsh-novel-writer/state`(loopback 围栏,可选局域网放行)。
42
-
43
- ## 安装
44
-
45
- ```sh
46
- dsh plugin --profile web add D:/tools/dsh-novel-writer
47
- ```
48
-
49
- 或手动:放入 profile 的 node_modules + `dsh.profile.bundles` 加 `dsh-novel-writer`;重启 web 应用。
50
-
51
- ## 配置
52
-
53
- ```yaml
54
- - id: novel-writer
55
- config:
56
- root: 'D:/我的小说库'
57
- sentenceAnalysis:
58
- enabled: true
59
- autoAnalyze: true
60
- allowLanState: false # true=局域网访问 GUI 时也允许保存开关
24
+ - **10 个工具全部带独立 UI 开关**(侧边栏「写作助手功能」面板):novel_books / novel_chapters / novel_read / novel_keywords / novel_new_chapter / novel_import / novel_sentence_analysis / novel_sentence_config / novel_style_check / novel_plot,默认全开;
25
+ - UI 更名:侧边栏入口/面板/横幅由「句式分析」改为「写作助手功能」;设置页卡片同步;
26
+ - novel_sentence_config 升级:支持 tools 参数(如 `{ novel_plot: false }`)与返回各工具开关状态;
27
+ - 关闭的工具调用时返回明确提示(模型可见)。
61
28
  ```
package/lib/analysis.js CHANGED
@@ -599,6 +599,7 @@ export function analyzeText(text, options = {}) {
599
599
  const guidance = buildGuidance(ratios, motifs, chapterPatterns);
600
600
 
601
601
  const fingerprint = buildFingerprint(categories, lengths, style, emotion, motifs);
602
+ const density = densityOf(text);
602
603
 
603
604
  return {
604
605
  totalChars,
@@ -612,6 +613,7 @@ export function analyzeText(text, options = {}) {
612
613
  emotion,
613
614
  chapterPatterns,
614
615
  guidance,
616
+ density,
615
617
  fingerprint
616
618
  };
617
619
  }
@@ -714,3 +716,48 @@ export function styleDiffs(a, b, threshold = 0.06) {
714
716
  }
715
717
  return diffs;
716
718
  }
719
+
720
+ /** v0.8.0 细节密度词表:动作动词/物件名词/感官词。 */
721
+ const DENSITY_ACTION_VERBS = /摸|拿|推|拉|走|跑|蹲|站|坐|转|伸|握|点|掀|翻|跪|爬|钻|抱|搂|抬|放|挂|摘|系|披|踩|迈|跨|扑|撞|躲|闪|看|盯|瞥|望|听|闻|尝|咬|喝|吃|敲|拍|捏|揉|擦|吹|按|拨|拧|捡|拾|丢|抛|接|递|塞|掏|捞|舀|倒|浇|洗|叠|铺|盖|关|开|掩|合/;
722
+ const DENSITY_OBJECT_WORDS = /油灯|灯|床|门|窗|桌|椅|凳|杯|碗|碟|盘|筷|勺|剑|刀|枪|书|本|纸|笔|衣|裙|帽|鞋|镜|烛台|蜡烛|铁罐|罐|盒|箱|马车|车|墙|柱|帘|毯|被|枕|簪|铃|钟|画|架|梯|绳|袋|篮|桶|壶|锅|铲|柴|炭|火|灰/;
723
+ const DENSITY_SENSE = {
724
+ visual: /看|见|映入|光|亮|暗|色|影|闪|照|映|目光|眼神|脸|面|状|样/,
725
+ auditory: /听|声|音|响|钟|脚步|敲|喊|叫|低语|呢喃|咕哝|咚|砰|咔/,
726
+ tactile: /凉|热|疼|痛|滑|糙|冷|暖|烫|冰|湿|干|硬|软|麻|痒/,
727
+ olfactory: /香|臭|味|气|腥|甜|苦|酸/,
728
+ temperature: /热|烫|凉|冷|暖|温/
729
+ };
730
+
731
+ /** 细节密度统计(v0.8.0):动作链/物件名词/感官词,按千字归一。 */
732
+ export function densityOf(text) {
733
+ const sentences = splitSentences(text);
734
+ const totalChars = text.replace(/\s/g, "").length;
735
+ let actionVerbs = 0;
736
+ let actionChainSentences = 0;
737
+ let objectHits = 0;
738
+ const sense = { visual: 0, auditory: 0, tactile: 0, olfactory: 0, temperature: 0 };
739
+ const actionChains = [];
740
+ for (const sentence of sentences) {
741
+ let verbs = 0;
742
+ for (const m of sentence.matchAll(new RegExp(DENSITY_ACTION_VERBS.source, "g"))) verbs += 1;
743
+ if (verbs >= 2) {
744
+ actionChainSentences += 1;
745
+ if (actionChains.length < 3) actionChains.push(sentence.slice(0, 40));
746
+ }
747
+ actionVerbs += verbs;
748
+ for (const m of sentence.matchAll(new RegExp(DENSITY_OBJECT_WORDS.source, "g"))) objectHits += 1;
749
+ for (const [kind, re] of Object.entries(DENSITY_SENSE)) {
750
+ for (const m of sentence.matchAll(new RegExp(re.source, "g"))) sense[kind] += 1;
751
+ }
752
+ }
753
+ const per1000 = (n) => Math.round((n / Math.max(totalChars, 1)) * 1000 * 10) / 10;
754
+ const ratio = sentences.length === 0 ? 0 : Math.round((actionChainSentences / sentences.length) * 1000) / 10;
755
+ return {
756
+ actionVerbsPer1000: per1000(actionVerbs),
757
+ actionChainRatio: ratio,
758
+ actionChainExamples: actionChains,
759
+ objectNounsPer1000: per1000(objectHits),
760
+ sensePer1000: per1000(Object.values(sense).reduce((x, y) => x + y, 0)),
761
+ sense: { ...sense }
762
+ };
763
+ }
package/lib/client.js CHANGED
@@ -36,6 +36,8 @@ window.__ModuleLoader__.load({
36
36
  ".nwPanelHeader{display:flex;align-items:center;gap:10px}" +
37
37
  ".nwPanelTitle{font-size:16px;font-weight:700;flex:1;min-width:0}" +
38
38
  ".nwClose{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);width:30px;height:30px;line-height:1}" +
39
+ ".nwRefresh{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:11px;line-height:1;padding:5px 9px}" +
40
+ ".nwRefresh:hover{background:var(--dsw-alias-bg-layer-2)}" +
39
41
  ".nwDesc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.6;margin:0}" +
40
42
  ".nwBanner{display:flex;align-items:center;gap:10px;border-radius:12px;padding:14px 16px;font-size:15px;font-weight:700;border:1px solid}" +
41
43
  ".nwBannerOn{background:rgba(34,197,94,.12);border-color:#22c55e;color:#15803d}" +
@@ -60,6 +62,24 @@ window.__ModuleLoader__.load({
60
62
  ".nwBadge{flex:none;border-radius:999px;padding:3px 10px;font-size:12px;font-weight:700;white-space:nowrap;letter-spacing:.5px}" +
61
63
  ".nwBadgeOn{background:#22c55e;color:#fff}" +
62
64
  ".nwBadgeOff{background:#e5e7eb;color:#6b7280}" +
65
+ ".nwSectionTitle{font-size:13px;font-weight:700;margin-top:6px}" +
66
+ ".nwToolsHint{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;margin:2px 0 4px}" +
67
+ ".nwToolRow{display:flex;align-items:center;gap:10px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:10px;padding:8px 12px}" +
68
+ ".nwToolRowOn{border-color:rgba(34,197,94,.55);background:rgba(34,197,94,.05)}" +
69
+ ".nwToolLabel{flex:1;min-width:0;font-size:12px;line-height:1.4;color:var(--dsw-alias-label-primary)}" +
70
+ ".nwSwitchSmall{appearance:none;flex:none;width:40px;height:22px;border-radius:999px;border:2px solid #9ca3af;background:#9ca3af;cursor:pointer;position:relative;padding:0;transition:background .16s,border-color .16s}" +
71
+ ".nwSwitchSmallOn{background:#22c55e;border-color:#22c55e}" +
72
+ ".nwSwitchSmallKnob{position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:#fff;transition:left .16s}" +
73
+ ".nwSwitchSmallOn .nwSwitchSmallKnob{left:20px}" +
74
+ ".nwToolDesc{font-size:11px;line-height:1.45;color:var(--dsw-alias-label-tertiary);margin-top:2px}" +
75
+ ".nwPlotBox{margin-top:6px;padding:6px 8px;background:rgba(127,127,127,.08);border-radius:8px}" +
76
+ ".nwPlotPath{font-size:11px;line-height:1.5;color:var(--dsw-alias-label-secondary);word-break:break-all}" +
77
+ ".nwBtnGroup{display:flex;gap:6px;margin-top:6px}" +
78
+ ".nwBtn{font-size:11px;line-height:1;padding:5px 10px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);cursor:pointer}" +
79
+ ".nwBtn:hover{background:var(--dsw-alias-bg-layer-2)}" +
80
+ ".nwBtn:disabled{opacity:.45;cursor:not-allowed}" +
81
+ ".nwPlotMsg{font-size:11px;line-height:1.5;color:var(--dsw-alias-label-secondary);margin-top:4px;word-break:break-all}" +
82
+ ".nwPlotErr{color:#ef4444}" +
63
83
  ".nwStatus{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;word-break:break-all}" +
64
84
  ".nwFoot{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.6;border-top:1px solid var(--dsw-alias-border-l2);padding-top:10px;margin-top:4px}";
65
85
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"dsh-novel-writer\"]") === null) {
@@ -72,61 +92,135 @@ window.__ModuleLoader__.load({
72
92
 
73
93
  // ---- 文案(zh / en)----
74
94
  var zh = {
75
- "entry.label": "句式分析",
76
- "entry.tooltip": "句式模式分析开关(dsh-novel-writer)",
77
- "panel.title": "句式模式分析",
78
- "panel.desc": "分析作品的句式分布(陈述/对话/心理/疑问/反问/感叹/祈使/省略留白)、排列规律、句长节奏、情感曲线与风格指纹,帮助 AI 快速掌握作者的写作习惯与主观情感。",
79
- "panel.enabled": "启用句式分析",
80
- "panel.enabledHint": "开启后,AI 可以调用 novel_sentence_analysis 提取句式模式报告。",
95
+ "entry.label": "写作助手功能",
96
+ "entry.tooltip": "写作助手功能开关(dsh-novel-writer):句式分析/风格自检/伏笔登记等",
97
+ "panel.title": "写作助手功能",
98
+ "panel.desc": "管理写作助手的功能开关:句式模式分析(九类句式/情感曲线/风格指纹)、风格自检、伏笔登记、稿件导入等。关闭的工具,AI 调用时会收到提示。",
99
+ "panel.enabled": "启用写作助手功能",
100
+ "panel.enabledHint": "总开关:控制句式模式分析与风格自检(novel_sentence_analysis / novel_style_check)。",
81
101
  "panel.autoAnalyze": "分析作品时自动使用",
82
102
  "panel.autoAnalyzeHint": "开启后,AI 在分析作品时会主动附带句式分析;关闭则仅在用户明确要求时使用。",
103
+ "panel.toolsTitle": "工具开关",
104
+ "panel.toolsHint": "全部默认开启。关闭后 AI 调用该工具会收到明确提示;可随时在此重新开启。",
83
105
  "panel.loading": "正在读取开关状态…",
84
106
  "panel.saved": "状态已保存到宿主端",
85
107
  "panel.localOnly": "宿主端不可达,开关仅保存在本浏览器(重启后可能恢复默认)。",
86
108
  "panel.saveFailed": "保存到宿主端失败,已降级保存在本浏览器。",
87
- "panel.foot": "提示:也可在对话中让 AI 用 novel_sentence_config 查看/修改这两个开关。重启 Web 应用后,AI 的工具列表中会出现 novel_sentence_analysis / novel_sentence_config。",
109
+ "panel.foot": "提示:也可在对话中让 AI 用 novel_sentence_config 查看/修改全部开关。",
88
110
  "panel.close": "关闭",
89
- "banner.on": "句式分析已启用",
90
- "banner.onSub": "AI 可以调用 novel_sentence_analysis",
91
- "banner.off": "句式分析已关闭",
92
- "banner.offSub": "AI 不会执行句式分析",
111
+ "banner.on": "写作助手功能已启用",
112
+ "banner.onSub": "句式分析 / 风格自检可用",
113
+ "banner.off": "写作助手功能已关闭",
114
+ "banner.offSub": "句式分析 / 风格自检不可用",
93
115
  "badge.on": "已开启",
94
116
  "badge.off": "已关闭",
117
+ "tool.novel_books": "作品列表 novel_books",
118
+ "tool.novel_chapters": "章节清单 novel_chapters",
119
+ "tool.novel_read": "阅读章节 novel_read",
120
+ "tool.novel_keywords": "关键词分析 novel_keywords",
121
+ "tool.novel_new_chapter": "新建章节 novel_new_chapter",
122
+ "tool.novel_import": "稿件导入 novel_import",
123
+ "tool.novel_sentence_analysis": "句式模式分析 novel_sentence_analysis",
124
+ "tool.novel_sentence_config": "开关配置 novel_sentence_config",
125
+ "tool.novel_style_check": "风格自检 novel_style_check",
126
+ "tool.novel_plot": "伏笔登记 novel_plot",
127
+ "tool.novel_books.desc": "列出书库中的全部作品(章节数/总字数)。",
128
+ "tool.novel_chapters.desc": "查看某本书的章节清单与字数。",
129
+ "tool.novel_read.desc": "阅读章节正文(支持分页与编码自动识别)。",
130
+ "tool.novel_keywords.desc": "提取高频关键词(词组/三字组/疑似人名)。",
131
+ "tool.novel_new_chapter.desc": "创建新章节文件(自动取下一章号)。",
132
+ "tool.novel_import.desc": "批量导入原稿件并自动分类到书库。",
133
+ "tool.novel_sentence_analysis.desc": "句式分布/排列规律/情感曲线/风格指纹。",
134
+ "tool.novel_sentence_config.desc": "查看/修改全部开关(AI 通道)。",
135
+ "tool.novel_style_check.desc": "本章文风与全书其余章节的相似度检查。",
136
+ "tool.novel_plot.desc": "登记/回收剧情伏笔,续写前查看未回收项。",
137
+ "tool.novel_settings": "设定管理 novel_settings",
138
+ "tool.novel_summary": "章节摘要 novel_summary",
139
+ "tool.novel_continuity_check": "连贯性审计 novel_continuity_check",
140
+ "tool.novel_settings.desc": "四张设定表:人物卡/地点卡/道具/时间线。",
141
+ "tool.novel_summary.desc": "保存/读取每章摘要,长书续写先读摘要。",
142
+ "tool.novel_continuity_check.desc": "对照设定表扫描全书,输出矛盾候选。",
143
+ "plot.pathLabel": "数据目录",
144
+ "plot.pathUnknown": "位于书库根的 .novel-writer 文件夹(plots 伏笔 / settings 设定 / summaries 摘要 / analysis 分析报告)。先让 AI 调用一次 novel_plot 或 novel_settings,此处会显示真实路径。",
145
+ "plot.open": "打开文件夹",
146
+ "plot.copy": "复制路径",
147
+ "plot.copied": "已复制",
148
+ "plot.revealOk": "已打开文件夹",
149
+ "plot.revealErr": "打开失败",
150
+ "panel.refresh": "刷新",
151
+ "panel.refreshed": "已刷新",
95
152
  "card.title": "小说写作助手 novel-writer",
96
153
  "card.desc": "句式模式分析:分析原文陈述/环境/心理/对话/疑问/反问/感叹等句子的排列节奏来辅助模仿文风。⚠️ 若机械套用导致文风僵硬,模型会优先回归自然表达。",
97
- "card.status": "句式模式分析:已启用 · 自动分析:开",
98
- "card.statusOff": "句式模式分析:已关闭",
99
- "card.statusAutoOff": "句式模式分析:已启用 · 自动分析:关",
100
- "card.hint": "开关统一在侧边栏「句式分析」面板(也可让 AI 用 novel_sentence_config 调整),本卡片仅显示状态。",
154
+ "card.status": "写作助手功能:已启用 · 自动分析:开",
155
+ "card.statusOff": "写作助手功能:已关闭",
156
+ "card.statusAutoOff": "写作助手功能:已启用 · 自动分析:关",
157
+ "card.hint": "全部开关统一在侧边栏「写作助手功能」面板(也可让 AI 用 novel_sentence_config 调整),本卡片仅显示状态。",
101
158
  "card.open": "打开开关面板"
102
159
  };
103
160
  var en = {
104
- "entry.label": "Sentence Analysis",
105
- "entry.tooltip": "Sentence-pattern analysis switch (dsh-novel-writer)",
106
- "panel.title": "Sentence Pattern Analysis",
107
- "panel.desc": "Analyzes sentence-type distribution (statement/dialogue/inner-thought/question/rhetoric/exclamation/imperative/ellipsis), arrangement patterns, rhythm, emotion curve and a style fingerprint — so the AI can quickly learn an author's habits and subjective tone.",
108
- "panel.enabled": "Enable sentence analysis",
109
- "panel.enabledHint": "When on, the AI may call novel_sentence_analysis for pattern reports.",
161
+ "entry.label": "Writing Assistant",
162
+ "entry.tooltip": "Writing assistant switches (dsh-novel-writer): analysis / style check / plot tracking etc.",
163
+ "panel.title": "Writing Assistant",
164
+ "panel.desc": "Manage the writing-assistant features: sentence-pattern analysis, style check, plot tracking, import, etc. Disabled tools return a clear notice when called.",
165
+ "panel.enabled": "Enable writing assistant",
166
+ "panel.enabledHint": "Master switch: controls novel_sentence_analysis and novel_style_check.",
110
167
  "panel.autoAnalyze": "Auto-use when analyzing works",
111
168
  "panel.autoAnalyzeHint": "When on, the AI proactively includes sentence analysis; otherwise only on explicit request.",
169
+ "panel.toolsTitle": "Tool switches",
170
+ "panel.toolsHint": "All on by default. Disabled tools return a clear notice; re-enable anytime here.",
112
171
  "panel.loading": "Loading switch state…",
113
172
  "panel.saved": "Saved to the host state file",
114
173
  "panel.localOnly": "Host unreachable; kept in this browser only (defaults may return after restart).",
115
174
  "panel.saveFailed": "Host save failed; fell back to this browser.",
116
- "panel.foot": "Tip: ask the AI to run novel_sentence_config to view/change these switches. After restarting the Web app, novel_sentence_analysis / novel_sentence_config appear in the tool list.",
175
+ "panel.foot": "Tip: ask the AI to run novel_sentence_config to view/change all switches.",
117
176
  "panel.close": "Close",
118
- "banner.on": "Sentence analysis is ON",
119
- "banner.onSub": "novel_sentence_analysis is available",
120
- "banner.off": "Sentence analysis is OFF",
121
- "banner.offSub": "the AI will not run it",
177
+ "banner.on": "Writing assistant is ON",
178
+ "banner.onSub": "analysis / style check available",
179
+ "banner.off": "Writing assistant is OFF",
180
+ "banner.offSub": "analysis / style check unavailable",
122
181
  "badge.on": "ON",
123
182
  "badge.off": "OFF",
183
+ "tool.novel_books": "Books novel_books",
184
+ "tool.novel_chapters": "Chapters novel_chapters",
185
+ "tool.novel_read": "Read novel_read",
186
+ "tool.novel_keywords": "Keywords novel_keywords",
187
+ "tool.novel_new_chapter": "New chapter novel_new_chapter",
188
+ "tool.novel_import": "Import novel_import",
189
+ "tool.novel_sentence_analysis": "Pattern analysis novel_sentence_analysis",
190
+ "tool.novel_sentence_config": "Config novel_sentence_config",
191
+ "tool.novel_style_check": "Style check novel_style_check",
192
+ "tool.novel_plot": "Plot tracking novel_plot",
193
+ "tool.novel_books.desc": "List all books (chapters / total chars).",
194
+ "tool.novel_chapters.desc": "List chapters of a book with sizes.",
195
+ "tool.novel_read.desc": "Read chapter text (paged, auto-encoding).",
196
+ "tool.novel_keywords.desc": "Extract keywords (bigrams / trigrams / name candidates).",
197
+ "tool.novel_new_chapter.desc": "Create a new chapter file (next number auto).",
198
+ "tool.novel_import.desc": "Import raw drafts and auto-classify into the library.",
199
+ "tool.novel_sentence_analysis.desc": "Pattern distribution / rhythm / emotion curve / fingerprint.",
200
+ "tool.novel_sentence_config.desc": "View / change all switches (AI channel).",
201
+ "tool.novel_style_check.desc": "Style similarity of this chapter vs the rest of the book.",
202
+ "tool.novel_plot.desc": "Track / close plot hooks; check before continuing.",
203
+ "tool.novel_settings": "Settings novel_settings",
204
+ "tool.novel_summary": "Summaries novel_summary",
205
+ "tool.novel_continuity_check": "Continuity novel_continuity_check",
206
+ "tool.novel_settings.desc": "Four tables: characters / locations / items / timeline.",
207
+ "tool.novel_summary.desc": "Store / read per-chapter summaries.",
208
+ "tool.novel_continuity_check.desc": "Scan against settings, list contradiction candidates.",
209
+ "plot.pathLabel": "Data dir",
210
+ "plot.pathUnknown": "Located in .novel-writer under the library root (plots / settings / summaries / analysis). Ask the AI to run novel_plot or novel_settings once to show the real path.",
211
+ "plot.open": "Open folder",
212
+ "plot.copy": "Copy path",
213
+ "plot.copied": "Copied",
214
+ "plot.revealOk": "Opened",
215
+ "plot.revealErr": "Open failed",
216
+ "panel.refresh": "Refresh",
217
+ "panel.refreshed": "Refreshed",
124
218
  "card.title": "Novel Writer (dsh-novel-writer)",
125
219
  "card.desc": "Sentence-pattern analysis: reads the rhythm of statements/environment/inner-thought/dialogue/questions etc. to help mimic the author's style. ⚠️ If mechanical imitation stiffens the prose, natural expression wins.",
126
- "card.status": "Sentence analysis: ON · auto-analyze: ON",
127
- "card.statusOff": "Sentence analysis: OFF",
128
- "card.statusAutoOff": "Sentence analysis: ON · auto-analyze: OFF",
129
- "card.hint": "Switches live in the sidebar 'Sentence Analysis' panel (or ask the AI to run novel_sentence_config); this card only shows status.",
220
+ "card.status": "Writing assistant: ON · auto-analyze: ON",
221
+ "card.statusOff": "Writing assistant: OFF",
222
+ "card.statusAutoOff": "Writing assistant: ON · auto-analyze: OFF",
223
+ "card.hint": "All switches live in the sidebar 'Writing Assistant' panel (or ask the AI to run novel_sentence_config); this card only shows status.",
130
224
  "card.open": "Open the switch panel"
131
225
  };
132
226
  function dictionary() {
@@ -137,6 +231,9 @@ window.__ModuleLoader__.load({
137
231
  return text === void 0 ? key : text;
138
232
  }
139
233
 
234
+ // ---- 工具清单(与宿主端 ALL_TOOLS 一致)----
235
+ var ALL_TOOLS = ["novel_books", "novel_chapters", "novel_read", "novel_keywords", "novel_new_chapter", "novel_import", "novel_sentence_analysis", "novel_sentence_config", "novel_style_check", "novel_plot", "novel_settings", "novel_summary", "novel_continuity_check"];
236
+
140
237
  // ---- 与宿主端同步 ----
141
238
  var STATE_ROUTE = "/api/dsh-novel-writer/state";
142
239
  var STORAGE_KEY = "dsh.novelWriter.state.v1";
@@ -161,12 +258,18 @@ window.__ModuleLoader__.load({
161
258
  var state = {
162
259
  enabled: true,
163
260
  autoAnalyze: true,
261
+ tools: {},
164
262
  file: "",
165
263
  source: "",
166
264
  loading: true,
167
265
  hostOk: true,
168
266
  saveFailed: false,
169
- panelOpen: false
267
+ panelOpen: false,
268
+ plotsDir: "",
269
+ dataDir: "",
270
+ revealMsg: "",
271
+ revealErr: false,
272
+ revealing: false
170
273
  };
171
274
  return {
172
275
  getSnapshot: function () { return state; },
@@ -177,6 +280,59 @@ window.__ModuleLoader__.load({
177
280
  subscribe: function (fn) {
178
281
  listeners.add(fn);
179
282
  return function () { listeners.delete(fn); };
283
+ },
284
+ refresh: function () {
285
+ return fetchState().then(function (remote) {
286
+ controller.set({
287
+ enabled: !!remote.enabled,
288
+ autoAnalyze: !!remote.autoAnalyze,
289
+ tools: remote.tools || {},
290
+ plotsDir: remote.plotsDir || "",
291
+ dataDir: remote.dataDir || "",
292
+ file: remote.file || "",
293
+ hostOk: true,
294
+ loading: false,
295
+ revealMsg: t("panel.refreshed"),
296
+ revealErr: false
297
+ });
298
+ return remote;
299
+ }).catch(function () {
300
+ controller.set({ hostOk: false, loading: false, revealMsg: t("plot.revealErr") + ":宿主端不可达", revealErr: true });
301
+ return null;
302
+ });
303
+ },
304
+ openDataDir: function () {
305
+ controller.set({ revealMsg: "", revealErr: false, revealing: true });
306
+ fetch("/api/dsh-novel-writer/reveal", {
307
+ method: "POST",
308
+ headers: { "content-type": "application/json" },
309
+ body: JSON.stringify({ target: "data-dir" })
310
+ }).then(function (r) { return r.json(); }).then(function (data) {
311
+ if (data.ok === true) {
312
+ controller.set({ dataDir: data.path || controller.getSnapshot().dataDir, revealMsg: t("plot.revealOk") + ":" + (data.path || ""), revealErr: false });
313
+ } else {
314
+ controller.set({ revealMsg: data.error || t("plot.revealErr"), revealErr: true });
315
+ }
316
+ }).catch(function () {
317
+ controller.set({ revealMsg: t("plot.revealErr") + ":宿主端不可达", revealErr: true });
318
+ }).finally(function () {
319
+ controller.set({ revealing: false });
320
+ });
321
+ },
322
+ copyPlotsPath: function () {
323
+ var dir = controller.getSnapshot().dataDir || controller.getSnapshot().plotsDir;
324
+ if (dir === "") {
325
+ controller.set({ revealMsg: t("plot.pathUnknown"), revealErr: true });
326
+ return;
327
+ }
328
+ var done = function () {
329
+ controller.set({ revealMsg: t("plot.copied") + ":" + dir, revealErr: false });
330
+ };
331
+ if (navigator.clipboard && navigator.clipboard.writeText) {
332
+ navigator.clipboard.writeText(dir).then(done, done);
333
+ } else {
334
+ done();
335
+ }
180
336
  }
181
337
  };
182
338
  }
@@ -289,6 +445,11 @@ window.__ModuleLoader__.load({
289
445
  "div",
290
446
  { className: "nwPanelHeader" },
291
447
  react.createElement("div", { className: "nwPanelTitle" }, t("panel.title")),
448
+ react.createElement(
449
+ "button",
450
+ { type: "button", className: "nwRefresh", onClick: function () { props.controller.refresh(); } },
451
+ t("panel.refresh")
452
+ ),
292
453
  react.createElement("button", { type: "button", className: "nwClose", "aria-label": t("panel.close"), onClick: props.onClose }, "×")
293
454
  ),
294
455
  react.createElement(
@@ -338,6 +499,58 @@ window.__ModuleLoader__.load({
338
499
  react.createElement("span", { className: "nwBadge " + (state.autoAnalyze ? "nwBadgeOn" : "nwBadgeOff") }, state.autoAnalyze ? t("badge.on") : t("badge.off"))
339
500
  )
340
501
  ),
502
+ react.createElement("div", { className: "nwSectionTitle" }, t("panel.toolsTitle")),
503
+ react.createElement("div", { className: "nwToolsHint" }, t("panel.toolsHint")),
504
+ ALL_TOOLS.map(function (name) {
505
+ var checked = (state.tools || {})[name] !== false;
506
+ var desc = t("tool." + name + ".desc");
507
+ var plotExtra = null;
508
+ if (name === "novel_plot") {
509
+ var dir = state.dataDir || state.plotsDir;
510
+ var pathText = dir !== "" ? dir : t("plot.pathUnknown");
511
+ plotExtra = react.createElement(
512
+ "div",
513
+ { className: "nwPlotBox" },
514
+ react.createElement("div", { className: "nwPlotPath", title: dir !== "" ? dir : "" }, t("plot.pathLabel") + ":" + pathText),
515
+ react.createElement(
516
+ "div",
517
+ { className: "nwBtnGroup" },
518
+ react.createElement("button", {
519
+ type: "button", className: "nwBtn", disabled: state.revealing,
520
+ onClick: function () { props.controller.openDataDir(); }
521
+ }, t("plot.open")),
522
+ react.createElement("button", {
523
+ type: "button", className: "nwBtn",
524
+ onClick: function () { props.controller.copyPlotsPath(); }
525
+ }, t("plot.copy"))
526
+ ),
527
+ state.revealMsg !== "" ? react.createElement("div", { className: "nwPlotMsg " + (state.revealErr ? "nwPlotErr" : "") }, state.revealMsg) : null
528
+ );
529
+ }
530
+ return react.createElement(
531
+ "div",
532
+ { className: "nwToolRow" + (checked ? " nwToolRowOn" : ""), key: name },
533
+ react.createElement(
534
+ "div",
535
+ { className: "nwToolLabel", title: desc },
536
+ react.createElement("div", null, t("tool." + name)),
537
+ react.createElement("div", { className: "nwToolDesc" }, desc),
538
+ plotExtra
539
+ ),
540
+ react.createElement(
541
+ "button",
542
+ {
543
+ type: "button",
544
+ role: "switch",
545
+ "aria-checked": checked ? "true" : "false",
546
+ className: "nwSwitchSmall" + (checked ? " nwSwitchSmallOn" : ""),
547
+ disabled: state.loading,
548
+ onClick: function () { props.toggle({ tools: (function (patch) { patch[name] = !checked; return patch; })({}) }); }
549
+ },
550
+ react.createElement("span", { className: "nwSwitchSmallKnob" })
551
+ )
552
+ );
553
+ }),
341
554
  react.createElement(
342
555
  "div",
343
556
  { className: "nwStatus" },
@@ -480,6 +693,9 @@ window.__ModuleLoader__.load({
480
693
  controller.set({
481
694
  enabled: !!remote.enabled,
482
695
  autoAnalyze: !!remote.autoAnalyze,
696
+ tools: remote.tools || {},
697
+ plotsDir: remote.plotsDir || "",
698
+ dataDir: remote.dataDir || "",
483
699
  file: remote.file || "",
484
700
  source: "host",
485
701
  hostOk: true,