dsh-novel-writer 5.1.0 → 5.2.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/README.en.md +12 -2
- package/README.md +12 -2
- package/cordis.patch.yml +8 -3
- package/lib/client.js +194 -17
- package/lib/core.js +15 -3
- package/package.json +3 -3
- package/server.json +2 -2
package/README.en.md
CHANGED
|
@@ -16,7 +16,9 @@ Sentence, emotion and semantic analysis all run **on your machine**: a 24MB Chin
|
|
|
16
16
|
**zero API cost, your manuscript never leaves the device**. Built for DeepSeek Harness (DSH); the same engine is also
|
|
17
17
|
exposed as a **stdio MCP server** for Claude Desktop / Cursor.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
🔗 **Website**: <https://siweina.github.io/dsh-novel-writer/> · **Technical docs (parameters and examples for all 18 tools)**: <https://siweina.github.io/dsh-novel-writer/tools/>
|
|
20
|
+
|
|
21
|
+
[Website](https://siweina.github.io/dsh-novel-writer/) · [Technical docs](https://siweina.github.io/dsh-novel-writer/tools/) · [Install](#install) · [The writing loop](#the-writing-loop-end-to-end) · [60-second start](#60-second-start) · [See the output](#see-the-output) · [The 18 tools](#provided-tools-18) · [MCP server](#mcp-server-usable-outside-dsh)
|
|
20
22
|
|
|
21
23
|
---
|
|
22
24
|
|
|
@@ -35,6 +37,8 @@ exposed as a **stdio MCP server** for Claude Desktop / Cursor.
|
|
|
35
37
|
> **Token-saving mode (new in v5.1.0)**: the sidebar's **Lean workflow** switch (or `novel_sentence_config` with `leanWorkflow=true`) keeps exactly one rule — **call tools on demand**: no automatic self-checks, no re-checking fix lists, no registry updates; report tools return the brief form unless you explicitly pass `brief` (style report: 181 chars vs 1725 for the full one).
|
|
36
38
|
>
|
|
37
39
|
> **v5.1.0 cost & feel fixes**: v5.0.0 had turned "always run a self-check after writing + the closing trio" and "mark every item + verify repeatedly" into mandatory checklists — one chapter went from 3 tool calls to 6, and revising took 30+. Together with "build sentences to the skeletons" and "fix any out-of-band dimension", that made prose stiff. This release makes all of it **on demand**: a chapter now takes **2 calls and up**, the fix-list cap dropped **40 → 12** with only the worst paragraph per dimension, and it now says plainly that **out-of-band ≠ defect** (keep lyrical / psychological / elliptical passages when they read fine — `mark` them `skip`).
|
|
40
|
+
>
|
|
41
|
+
> **v5.2.0 official sidebar integration**: the browser half no longer injects DOM into the host. It uses the DSH 0.2.0 seats — `ctx.sidebarRightTabs.register` declares the tab type, `sidebar.right.pane.tab` provides the body (tab chip, docking, splitting, per-session layout and theming all belong to the host), `sidebar.footer.action` adds the left-sidebar entry, and `sidebar.right.pane.tab.title` keeps an open tab's label following language changes. Hosts without these services fall back to the old path automatically; tools, parameters and the state file are unchanged (still 18 tools).
|
|
38
42
|
|
|
39
43
|
## What problem does it solve
|
|
40
44
|
|
|
@@ -171,7 +175,7 @@ fixAnchors:3 条原著锚段(对话 / 心理 / 描写各一条,供逐句
|
|
|
171
175
|
6. **Emotion purification & quantification**: strong/weak emotion-word grading, pollution detection, caveat warning + AI re-verification; Valence sliding window → variance V / delta Δ / conflict index C + implicit imagery carriers.
|
|
172
176
|
7. **Worldview & pragmatics detection**: auto cultural-baseline detection with confidence; speechStyle title/honorifics/rituals/tone norms; genre & theme + webnovel signals.
|
|
173
177
|
8. **Writing toolkit**: plot tracking / five settings tables (characters·locations·items·timeline·worldview) / chapter summaries / continuity audit / batch import / style check / continuation writing.
|
|
174
|
-
9. **Per-tool UI toggles**: 「写作助手功能」 ("Writing Assistant") sidebar panel (master + grouped tool toggles + feature toggles + prompt mode/scene/**lean workflow**), plain-language labels, data-dir usage & semantic-engine status display.
|
|
178
|
+
9. **Per-tool UI toggles**: 「写作助手功能」 ("Writing Assistant") sidebar panel (master + grouped tool toggles + feature toggles + prompt mode/scene/**lean workflow**), plain-language labels, data-dir usage & semantic-engine status display.**Since v5.2.0 the panel uses the official DSH 0.2.0 seats** — it is a regular tab in the right sidebar (dock, split, float; layout persists per session), opened from an icon next to Settings in the left sidebar (icon + label when wide, icon only in the 56px rail) and listed on the “+ → guide” page; hosts older than 0.2.0 fall back to the previous mounting path automatically.
|
|
175
179
|
10. **Style Baseline**: Six writing metrics (syntactic complexity / modifier density / abstraction / action density / hedging / gap index) + per-chapter μ±σ baseline band; `novel_style_report` outputs the band, `novel_style_check` compares new chapters (in-band ✓ / out-of-band ⚠); per-metric ±% tolerance configurable in the sidebar (**recommended = 1.5× σ of the book's chapter variance**, rounded, clamped to ±10%~100%; leave blank to use recommended) — free theme, writing style kept inside the band.
|
|
176
180
|
11. **Writing sentinels**: `novel_continuity_check` extended — ①**bridge check** (`chapter`: time jumps / semantic distance / character continuity / hook handoff, with quoted evidence) ②**OOC check** (`ooc`: per-character emotion baseline deviation) ③**outline drift** (`outline`: direction vs body keyword overlap); **brief mode** for report tools.
|
|
177
181
|
12. **Original mode & creation files**: fill in creation settings in the sidebar (worldview/characters/forbidden/main conflict/genre/extras, blank = model decides, per-book profile library); novel_outline maintains creation files (bible/characters/outline/hooks/status), enforcing the bible → outline → hook chain with dynamic batches (10→20→30 chapters) to prevent plot jumps and OOC.
|
|
@@ -181,6 +185,8 @@ fixAnchors:3 条原著锚段(对话 / 心理 / 描写各一条,供逐句
|
|
|
181
185
|
|
|
182
186
|
## Provided Tools (18)
|
|
183
187
|
|
|
188
|
+
> Parameters, return shapes and worked examples for every tool live in the [online tool manual](https://siweina.github.io/dsh-novel-writer/tools/).
|
|
189
|
+
|
|
184
190
|
| Tool | Description |
|
|
185
191
|
|------|-------------|
|
|
186
192
|
| `novel_books` | List all books in library |
|
|
@@ -292,3 +298,7 @@ tool results; a plugin load failure cannot affect the DSH host process.
|
|
|
292
298
|
## License
|
|
293
299
|
|
|
294
300
|
[MIT](./LICENSE)
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
🔗 [Website](https://siweina.github.io/dsh-novel-writer/) · [Technical docs](https://siweina.github.io/dsh-novel-writer/tools/) · [GitHub](https://github.com/siweina/dsh-novel-writer) · [npm](https://www.npmjs.com/package/dsh-novel-writer) · [MCP server manual](./mcp/README.md)
|
package/README.md
CHANGED
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
句式 / 情感 / 语义全部**在本地算**:24MB 中文模型随包分发,**零 API 花费、正文不出本机**。
|
|
16
16
|
为 DeepSeek Harness(DSH)打造;同一套能力也可作为 **MCP 服务器**给 Claude Desktop / Cursor 使用。
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
🔗 **官网**:<https://siweina.github.io/dsh-novel-writer/> · **技术文档(18 个工具的参数与示例)**:<https://siweina.github.io/dsh-novel-writer/tools/>
|
|
19
|
+
|
|
20
|
+
[官网](https://siweina.github.io/dsh-novel-writer/) · [技术文档](https://siweina.github.io/dsh-novel-writer/tools/) · [安装](#安装) · [写作流程](#写作流程一条能走完的闭环) · [60 秒上手](#60-秒上手) · [看看输出](#看看输出长什么样) · [18 个工具](#提供的工具18-个) · [MCP 服务器](#mcp-服务器非-dsh-用户也能用)
|
|
19
21
|
|
|
20
22
|
---
|
|
21
23
|
|
|
@@ -34,6 +36,8 @@
|
|
|
34
36
|
> **省 token 模式(v5.1.0 新增)**:侧边栏「精简工作流」开关(或 `novel_sentence_config` 传 `leanWorkflow=true`)打开后只保留一条原则——**工具按需调用**:不主动跑自检、不复测清单、不登记伏笔/钩子/摘要;报告类工具未显式传 `brief` 时默认走精简输出(风格画像实测 181 字符 vs 完整 1725 字符)。
|
|
35
37
|
>
|
|
36
38
|
> **v5.1.0 成本与手感修复**:v5.0.0 曾把「写完必须自检 + 收尾三件」「改稿逐条 mark + 反复 verify」写成必做清单,一章从 3 次调用涨到 6 次、改稿要 30+ 次;同时"照骨架造句"与"任一维度出带必改"会让文章越改越僵。本版全部改回**按需**:写一章 **2 次调用起**,改稿清单上限 **40 → 12 条**、同一维度只报最严重的 1 段,并明确了**出带不等于缺陷**(抒情/心理/留白段读起来不别扭就保留,可用 `mark` 标 `skip`)。
|
|
39
|
+
>
|
|
40
|
+
> **v5.2.0 官方侧边栏适配**:浏览器半边不再往宿主 DOM 里塞东西。改用 DSH 0.2.0 的官方席位——`ctx.sidebarRightTabs.register` 声明 tab 类型 + `sidebar.right.pane.tab` 提供正文(tab chip、停靠、分栏、每会话布局、深浅主题全归宿主)+ `sidebar.footer.action` 放左栏入口 + `sidebar.right.pane.tab.title` 让已打开的 tab 跟随语言切换。检测不到这些服务的宿主自动回退旧路径;工具、参数、状态文件完全不变(仍 18 个工具)。
|
|
37
41
|
|
|
38
42
|
## 它解决什么问题
|
|
39
43
|
|
|
@@ -168,7 +172,7 @@ fixAnchors:3 条原著锚段(对话 / 心理 / 描写各一条,供逐句
|
|
|
168
172
|
6. **情感净化 + 量化**:强/弱情绪词分级、污染源检测、caveat 预警 + AI 复核;Valence 滑动窗口 → 方差 V / 斜率 Δ / 矛盾指数 C + 隐性意象载体。
|
|
169
173
|
7. **世界观与语用检测**:文化基准自动判断(西/东/混合)+ 置信度;speechStyle 称谓/客套/仪式/语气规范;题材流派 + 网文信号。
|
|
170
174
|
8. **写作辅助全家桶**:伏笔登记表 / 设定五张表(人物·地点·道具·时间线·世界观)/ 章节摘要 / 连贯性审计 / 批量导入 / 风格自检 / 续写辅助。
|
|
171
|
-
9. **全工具 UI 开关**:侧边栏「写作助手功能」面板(总开关 + 工具开关分组 + 功能开关 +
|
|
175
|
+
9. **全工具 UI 开关**:侧边栏「写作助手功能」面板(总开关 + 工具开关分组 + 功能开关 + 提示词档位/场景/**精简工作流**),大白话文案,显示数据目录占用与语义引擎状态。**v5.2.0 起面板走 DSH 0.2.0 官方席位**——作为右栏的一个正规 tab(可停靠 / 分栏 / 浮动,布局随会话持久化),入口是左栏设置旁的图标(宽栏 = 图标 + 文字、56px 轨道 = 纯图标),侧栏「+ → 引导页」里也有入口;老宿主(< 0.2.0)自动回退旧的侧栏挂载方式。
|
|
172
176
|
10. **风格基线**:文笔六维测量(句法复杂度/修饰密度/抽象度/动作密度/不确定性/留白指数)+ 按章节 μ±σ 基线带;`novel_style_report` 输出基线带,`novel_style_check` 对照新章偏差(带内 ✓ / 出带 ⚠);侧边栏可自定义每维 ±% 容差(**推荐值 = 原著章节波动的 1.5 倍 σ**,自动取整、限 ±10%~100%;输入框留空即用推荐)——原创/续写时主题自由、写法保持在基线带内。
|
|
173
177
|
11. **写作哨兵三件套**:`novel_continuity_check` 扩展——①**衔接检查**(chapter 参数:时间硬跳/语义距离/人物延续/钩子承接四路检测,带原文引用)②**OOC 检测**(ooc 参数:角色情绪基线偏离)③**大纲走偏**(outline 参数:方向行 vs 正文关键词重合);报告工具支持 **brief 精简模式**。
|
|
174
178
|
12. **原创模式与创作资料**:侧边栏填写创作设定(世界观/角色/禁忌/主线/题材/额外要求,留空=模型自定,多书独立设定库);novel_outline 维护创作资料(创作设定/人物/剧情大纲/钩子记录/创作状态卡),原创强制「设定书→大纲→钩子」链,动态批次(10→20→30 章)防剧情跳跃与角色 OOC。
|
|
@@ -178,6 +182,8 @@ fixAnchors:3 条原著锚段(对话 / 心理 / 描写各一条,供逐句
|
|
|
178
182
|
|
|
179
183
|
## 提供的工具(18 个)
|
|
180
184
|
|
|
185
|
+
> 每个工具的参数、返回结构与示例,见[在线工具手册](https://siweina.github.io/dsh-novel-writer/tools/)。
|
|
186
|
+
|
|
181
187
|
| 工具 | 说明 |
|
|
182
188
|
|------|------|
|
|
183
189
|
| `novel_books` | 列出章节库全部作品 |
|
|
@@ -290,3 +296,7 @@ npx -y -p dsh-novel-writer dsh-novel-writer-mcp --root /你的小说库路径
|
|
|
290
296
|
## 许可证
|
|
291
297
|
|
|
292
298
|
[MIT](./LICENSE)
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
🔗 [官网](https://siweina.github.io/dsh-novel-writer/) · [技术文档](https://siweina.github.io/dsh-novel-writer/tools/) · [GitHub](https://github.com/siweina/dsh-novel-writer) · [npm](https://www.npmjs.com/package/dsh-novel-writer) · [MCP 服务器手册](./mcp/README.md)
|
package/cordis.patch.yml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# dsh-novel-writer v5.
|
|
1
|
+
# dsh-novel-writer v5.2.0 bundle patch: 18 tools (incl. novel_semantic_search) + local embedding engine.
|
|
2
2
|
# dsh-novel-writer bundle patch: mounts the novel-writing assistant plugin
|
|
3
3
|
# into the profile's loader tree. The entry's name is the plugin package
|
|
4
4
|
# itself, resolved from the profile's node_modules.
|
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
# v0.3.0: the same row now carries BOTH halves — the node half (exports ".")
|
|
7
7
|
# runs in the host process (novel_* tools + /api/dsh-novel-writer/state
|
|
8
8
|
# routes for the UI switch), and the dsh.client declaration in package.json
|
|
9
|
-
# makes the browser half (exports "./client") load in the web GUI
|
|
10
|
-
#
|
|
9
|
+
# makes the browser half (exports "./client") load in the web GUI.
|
|
10
|
+
#
|
|
11
|
+
# v5.2.0: the browser half no longer injects DOM into the host's sidebar
|
|
12
|
+
# column. It registers official seats instead — a right-sidebar tab type
|
|
13
|
+
# (ctx.sidebarRightTabs.register + sidebar.right.pane.tab) plus a footer
|
|
14
|
+
# entry (sidebar.footer.action). Hosts without those services (pre-0.2.0)
|
|
15
|
+
# fall back to the old DOM-injection path automatically.
|
|
11
16
|
- insert:
|
|
12
17
|
- id: novel-writer
|
|
13
18
|
name: 'dsh-novel-writer'
|
package/lib/client.js
CHANGED
|
@@ -134,13 +134,29 @@ var css = `
|
|
|
134
134
|
.nwRawDanger{color:#ef4444!important;font-weight:600}
|
|
135
135
|
.nwRawOn{font-size:11px;color:#ef4444;margin-top:2px;font-weight:600}
|
|
136
136
|
|
|
137
|
-
/* =====
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
/* ===== 确认框(v5.2.0:面板内卡片,不再用全屏遮罩)===== */
|
|
138
|
+
/* 旧版写死 rgba(0,0,0,.5)、上一版换成宿主 mask token + backdrop-blur —— 两者都是"整屏遮罩",
|
|
139
|
+
会把整个应用(侧栏、会话、面板)糊成一片均匀的灰,用户直观感受就是"背景突变了"。
|
|
140
|
+
但这两个确认框(先确认开启 / 再输入承诺文字)本来就是 PanelView 正文的一部分:
|
|
141
|
+
它们出现时已经替换掉正常正文,所以**完全不需要遮罩**——做成一枚居中卡片即可,
|
|
142
|
+
背景一点不变,也不会盖住侧栏与会话。 */
|
|
143
|
+
.nwModal{display:block;margin:18px auto 0;width:100%;max-width:460px}
|
|
144
|
+
.nwModalBox{background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2,transparent);border-radius:16px;padding:18px 20px;box-shadow:var(--dsw-shadow-lv3,0 12px 40px rgba(15,23,42,.18))}
|
|
145
|
+
.nwModalTitle{font-size:15px;font-weight:600;line-height:1.5;margin-bottom:8px}
|
|
146
|
+
.nwModalText{font-size:13px;line-height:1.7;color:var(--dsw-alias-label-secondary);white-space:pre-line;margin-bottom:14px}
|
|
147
|
+
.nwModalInput{width:100%;box-sizing:border-box;padding:9px 11px;font:inherit;font-size:13px;border:1px solid var(--dsw-alias-border-l2,#d4d4d8);border-radius:10px;background:var(--dsw-alias-bg-layer-1,#fafafa);color:var(--dsw-alias-label-primary);margin-bottom:14px}
|
|
148
|
+
.nwModalInput:focus{outline:none;border-color:var(--dsw-alias-brand-primary,#6366f1);box-shadow:0 0 0 3px rgba(99,102,241,.16)}
|
|
143
149
|
.nwModalBtns{display:flex;gap:10px;justify-content:flex-end}
|
|
150
|
+
/* 卡片里的按钮:给足点击面积,并与卡片底色拉开层次 */
|
|
151
|
+
.nwModalBtns .nwBtn{min-width:88px;height:34px;padding:0 14px;font-size:13px;border-radius:10px;background:var(--dsw-alias-bg-layer-1,#fff)}
|
|
152
|
+
.nwModalBtns .nwBtn:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-3,#f4f4f5))}
|
|
153
|
+
.nwModalBtns .nwBtnDanger{background:var(--dsw-alias-state-error-primary,#ef4444);border-color:transparent;color:#fff}
|
|
154
|
+
.nwModalBtns .nwBtnDanger:hover{filter:brightness(1.05)}
|
|
155
|
+
/* 入场:卡片轻微上浮即可,不再有整屏淡入(并尊重系统「减少动态效果」设置) */
|
|
156
|
+
@keyframes nwModalBoxIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
|
|
157
|
+
@media (prefers-reduced-motion: no-preference){
|
|
158
|
+
.nwModalBox{animation:nwModalBoxIn .18s ease-out}
|
|
159
|
+
}
|
|
144
160
|
|
|
145
161
|
/* ===== 工具行/开关/说明 ===== */
|
|
146
162
|
.nwToolItem{width:100%;margin-bottom:2px}
|
|
@@ -164,6 +180,10 @@ var css = `
|
|
|
164
180
|
.nwToolGroupHeadBtn{flex:1;display:flex;align-items:center;gap:8px;background:none;border:none;cursor:pointer;padding:0;text-align:left;color:inherit;font-size:13px}
|
|
165
181
|
.nwToolGroupHeadText{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
|
|
166
182
|
.nwToolGroupDescInline{font-size:11px;line-height:1.4;color:var(--dsw-alias-label-tertiary);font-weight:400;white-space:normal}
|
|
183
|
+
|
|
184
|
+
/* ===== v5.2.0:官方主栏全局面板 ===== */
|
|
185
|
+
/* 主栏页面:栅格、滚动容器与选中态都由宿主 layout 负责,这里只管内容排版 */
|
|
186
|
+
.nwPanelMain{max-width:760px;margin:0 auto;padding:28px 24px 48px;gap:10px;box-sizing:border-box;height:100%;overflow:auto}
|
|
167
187
|
`;
|
|
168
188
|
// v3.0.0-UI3:已存在旧 style 标签也覆盖内容——否则插件脚本重复执行时新 CSS 永不注入(UI 停留在旧版)
|
|
169
189
|
if (typeof document !== "undefined") {
|
|
@@ -195,6 +215,8 @@ var css = `
|
|
|
195
215
|
"sp.full": "完整",
|
|
196
216
|
"panel.spScene": "提示词场景",
|
|
197
217
|
"panel.spSceneHint": "完整档下注入哪一套流程:通用(默认,同旧版)/ 写新章 / 改稿 / 审计 / 建资料。场景只改变提示词内容,不影响工具能力。",
|
|
218
|
+
// v5.1.1:档位非「完整」时,场景按钮此前是 disabled(点不动且无解释)——现在始终可点,改用这行说明"为什么暂时不生效"
|
|
219
|
+
"panel.spSceneInactive": "当前档位不是「完整」,场景已保存但暂不注入——把上面的档位切到「完整」即生效。",
|
|
198
220
|
"sc.general": "通用",
|
|
199
221
|
"sc.writing": "写新章",
|
|
200
222
|
"sc.revising": "改稿",
|
|
@@ -409,6 +431,8 @@ var css = `
|
|
|
409
431
|
"sp.full": "Full",
|
|
410
432
|
"panel.spScene": "Prompt scene",
|
|
411
433
|
"panel.spSceneHint": "Which workflow Full mode injects: General (default, same as before) / Drafting / Revising / Auditing / Setup. Affects prompt text only, not tool capabilities.",
|
|
434
|
+
// v5.1.1: the scene buttons used to be disabled when the mode wasn't Full (no explanation) — now always clickable, with this note
|
|
435
|
+
"panel.spSceneInactive": "The mode above isn't Full, so the scene is saved but not injected yet — switch the mode to Full to activate it.",
|
|
412
436
|
"sc.general": "General",
|
|
413
437
|
"sc.writing": "Drafting",
|
|
414
438
|
"sc.revising": "Revising",
|
|
@@ -1637,17 +1661,19 @@ var TOOL_GROUPS = [
|
|
|
1637
1661
|
)
|
|
1638
1662
|
),
|
|
1639
1663
|
// v5.0.0:提示词场景——与上面的档位正交:档位决定"注入多少",场景决定"注入什么"。
|
|
1640
|
-
//
|
|
1664
|
+
// v5.1.1 修正:**不再因为档位非 full 就把按钮 disabled**——用户反馈"场景里点不了其他选项",
|
|
1665
|
+
// 因为默认档位是 off/精简,五个按钮全灰且没有任何解释,等于把"选场景"锁死在一个看不见的前提后面。
|
|
1666
|
+
// 现在按钮始终可点(只受 loading 限制),档位非 full 时改用行内提示说明"已保存但暂不注入"。
|
|
1641
1667
|
// 场景枚举必须与 lib/prompts.js 的 PROMPT_SCENES 保持一致(客户端是独立 bundle,不能 import)。
|
|
1642
1668
|
el("div", { className: "nwRow " + (state.systemPromptMode === "full" ? "nwRowOn" : "nwRowOff") },
|
|
1643
1669
|
el("div", { className: "nwRowText" },
|
|
1644
1670
|
el("div", { className: "nwRowLabel" }, t("panel.spScene")),
|
|
1645
|
-
el("div", { className: "nwRowHint" }, t("panel.spSceneHint"))
|
|
1671
|
+
el("div", { className: "nwRowHint" }, t("panel.spSceneHint") + (state.systemPromptMode === "full" ? "" : " " + t("panel.spSceneInactive")))
|
|
1646
1672
|
),
|
|
1647
1673
|
el("div", { className: "nwSeg" },
|
|
1648
1674
|
["general", "writing", "revising", "auditing", "setup"].map(function (s) {
|
|
1649
1675
|
var cur = state.promptScene || "general";
|
|
1650
|
-
return el("button", { key: s, type: "button", className: "nwSegBtn" + (cur === s ? " nwSegBtnOn" : ""), disabled: state.loading
|
|
1676
|
+
return el("button", { key: s, type: "button", className: "nwSegBtn" + (cur === s ? " nwSegBtnOn" : ""), disabled: state.loading,
|
|
1651
1677
|
onClick: function () { if (cur !== s) props.toggle({ promptScene: s }); } }, t("sc." + s));
|
|
1652
1678
|
})
|
|
1653
1679
|
)
|
|
@@ -1790,11 +1816,12 @@ var TOOL_GROUPS = [
|
|
|
1790
1816
|
)
|
|
1791
1817
|
);
|
|
1792
1818
|
}
|
|
1793
|
-
return el("div", { className: "nwPanel" },
|
|
1819
|
+
return el("div", { className: "nwPanel" + (props.variant === "main" ? " nwPanelMain" : "") },
|
|
1794
1820
|
el("div", { className: "nwPanelHeader" },
|
|
1795
1821
|
el("div", { className: "nwPanelTitle" }, t("panel.title") + (view !== "main" ? " › " + (view === "features" ? t("panel.featuresTitle") : view === "tools" ? t("panel.toolsTitle") : view === "baseline" ? t("panel.baselineTitle") : view === "creation" ? t("panel.creationTitle") : view === "creation-form" ? t("panel.creationTitle") + " · " + t("creation.formTitle") : view === "reports" ? t("reports.title") : t("model.title")) : "")),
|
|
1796
1822
|
el("button", { type: "button", className: "nwRefresh" + (state.refreshing ? " nwRefreshSpin" : ""), onClick: function () { props.controller.refresh(); } }, state.refreshing ? "⟳" : t("panel.refresh")),
|
|
1797
|
-
|
|
1823
|
+
// v5.2.0:作为官方右栏 tab 时不画关闭按钮——关闭由宿主 tab chip 提供
|
|
1824
|
+
typeof props.onClose === "function" ? el("button", { type: "button", className: "nwClose", "aria-label": t("panel.close"), onClick: props.onClose }, "×") : null
|
|
1798
1825
|
),
|
|
1799
1826
|
el("div", { className: "nwBanner " + (on ? "nwBannerOn" : "nwBannerOff") },
|
|
1800
1827
|
el("span", { className: "nwBannerIcon" }, on ? "✔" : "✘"),
|
|
@@ -1943,6 +1970,120 @@ var TOOL_GROUPS = [
|
|
|
1943
1970
|
};
|
|
1944
1971
|
}
|
|
1945
1972
|
|
|
1973
|
+
// ---- v5.2.0:官方席位(侧栏一行 + 主栏页面)----
|
|
1974
|
+
// 背景:v5.1.1 及以前是 DOM 注入——querySelector('[class*="sidebarCol"]') 找宿主侧栏列,
|
|
1975
|
+
// 自建容器 + 自写 CSS 模拟宿主样式。宿主一改版就失效,深色主题也对不上。
|
|
1976
|
+
// 本版改走 DSH 0.2.0 的官方席位,也就是「插件」「任务看板」所在的那一排:
|
|
1977
|
+
// ① sidebar.panellist(root 作用域 list):侧栏主导航区的一行,提供 id / order / label;
|
|
1978
|
+
// ② main(root 作用域 keyed):**同一个 id** 寻址主栏页面——行盒子、选中态、折叠提示、
|
|
1979
|
+
// 无障碍名称全由宿主 layout 服务负责,我们只画图标与页面内容。
|
|
1980
|
+
// 两个席位都由 shell 包声明,因此一律用 **ctx.slots.inject** 注册:它只在席位被声明后
|
|
1981
|
+
// 回调,未声明的宿主上什么都不发生(旧路径照常),**不会影响宿主启动**。
|
|
1982
|
+
// 注意区分:ctx.slots.inject(槽位等待,安全)≠ ctx.inject(cordis 服务等待,危险)。
|
|
1983
|
+
// 真机事故:v5.2.0 首版曾用 ctx.inject 等 sidebarRight/sidebarRightTabs,客户端宿主把
|
|
1984
|
+
// "等待未满足依赖的 entry" 判为 did not activate,桌面端直接启动失败——该写法已彻底删除,
|
|
1985
|
+
// 插件永远不能拖垮宿主启动。
|
|
1986
|
+
var NW_PANEL_ID = "dsh-novel-writer";
|
|
1987
|
+
var NW_PANEL_ORDER = 25; // 任务看板=20、skill-explorer=30、ssh=40:我们排在任务看板之后
|
|
1988
|
+
|
|
1989
|
+
/** 安全读取宿主服务:ctx.get(name) 优先(可读到未在 inject 声明的服务),再退回属性访问。 */
|
|
1990
|
+
function readHostService(ctx, name) {
|
|
1991
|
+
try {
|
|
1992
|
+
if (ctx && typeof ctx.get === "function") return ctx.get(name);
|
|
1993
|
+
} catch { /* 未注册的服务:ctx.get 可能抛错 */ }
|
|
1994
|
+
try { return ctx ? ctx[name] : void 0; } catch { /* 属性访问也可能被代理拦截 */ }
|
|
1995
|
+
return void 0;
|
|
1996
|
+
}
|
|
1997
|
+
/**
|
|
1998
|
+
* 槽位是否已被宿主声明。未声明的槽位 `register` 会抛错、`inject` 也不会回调,
|
|
1999
|
+
* 而"回调被同步调用"并不等于注册成功(测试桩与部分实现都会无脑调用回调)——
|
|
2000
|
+
* 所以接管前必须先问声明表:`specDynamic(key)` 未声明时返回 undefined。
|
|
2001
|
+
* 缺探测 API 的宿主(老版本)返回 false,宁可保留旧 UI 也不要挂空。
|
|
2002
|
+
*/
|
|
2003
|
+
function slotDeclared(ctx, name) {
|
|
2004
|
+
try {
|
|
2005
|
+
var slots = ctx && ctx.slots;
|
|
2006
|
+
if (!slots) return false;
|
|
2007
|
+
if (typeof slots.specDynamic === "function") return !!slots.specDynamic(name);
|
|
2008
|
+
if (typeof slots.spec === "function") return !!slots.spec(name);
|
|
2009
|
+
} catch { /* 探测失败按未声明处理 */ }
|
|
2010
|
+
return false;
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* 订阅控制器并强制重渲染。
|
|
2014
|
+
* 不能用 useSyncExternalStore:控制器的 getSnapshot() 返回同一个可变对象,
|
|
2015
|
+
* React 按引用比较会认为"没变化",界面不刷新。
|
|
2016
|
+
*/
|
|
2017
|
+
function useControllerTick(controller) {
|
|
2018
|
+
var force = react.useState(0)[1];
|
|
2019
|
+
react.useEffect(function () {
|
|
2020
|
+
return controller.subscribe(function () { force(function (n) { return n + 1; }); });
|
|
2021
|
+
}, [controller]);
|
|
2022
|
+
}
|
|
2023
|
+
/** 侧栏主导航那一行的图标(owner props: { size, active })——行盒子/文字/选中态由宿主画。 */
|
|
2024
|
+
function SidebarPanelIcon(props) {
|
|
2025
|
+
var size = typeof props.size === "number" && props.size > 0 ? props.size : 16;
|
|
2026
|
+
return react.createElement(
|
|
2027
|
+
"svg",
|
|
2028
|
+
{
|
|
2029
|
+
width: size, height: size, viewBox: "0 0 24 24", fill: "none",
|
|
2030
|
+
stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round",
|
|
2031
|
+
"aria-hidden": "true"
|
|
2032
|
+
},
|
|
2033
|
+
react.createElement("path", { d: "M5 20h14" }),
|
|
2034
|
+
react.createElement("path", { d: "M15.8 3.8a2.1 2.1 0 0 1 3 3L9.2 16.4 4.6 17.6l1.2-4.6z" })
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
/** 主栏页面(main keyed 席位):选中/切换由宿主负责,我们只渲染内容。 */
|
|
2038
|
+
function MainPanelPage(props) {
|
|
2039
|
+
useControllerTick(props.controller);
|
|
2040
|
+
return react.createElement(PanelView, {
|
|
2041
|
+
controller: props.controller,
|
|
2042
|
+
toggle: props.toggle,
|
|
2043
|
+
variant: "main",
|
|
2044
|
+
onClose: props.onClose
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* 注册官方席位:侧栏一行 + 主栏页面(同一个 id)。
|
|
2049
|
+
* 两个席位都用 ctx.slots.inject 等声明——未声明的宿主上回调不触发,旧路径保留。
|
|
2050
|
+
* 页面先挂成功才撤旧 UI(宁可暂时叠着,也不能出现"入口在、面板没了")。
|
|
2051
|
+
* @returns 是否已接管
|
|
2052
|
+
*/
|
|
2053
|
+
function registerOfficialPanel(ctx, controller, toggle, disposers) {
|
|
2054
|
+
if (!ctx.slots || typeof ctx.slots.inject !== "function") return false;
|
|
2055
|
+
if (!slotDeclared(ctx, "sidebar.panellist") || !slotDeclared(ctx, "main")) return false;
|
|
2056
|
+
var pageDispose = null;
|
|
2057
|
+
try {
|
|
2058
|
+
var layout = readHostService(ctx, "layout");
|
|
2059
|
+
var onClose = layout && typeof layout.selectPanel === "function"
|
|
2060
|
+
? function () { try { layout.selectPanel(null); } catch { /* ignore */ } }
|
|
2061
|
+
: null;
|
|
2062
|
+
pageDispose = ctx.slots.inject("main", function () {
|
|
2063
|
+
return ctx.slots.register({
|
|
2064
|
+
name: "main",
|
|
2065
|
+
key: NW_PANEL_ID,
|
|
2066
|
+
inject: function () { return { controller: controller, toggle: toggle, onClose: onClose }; }
|
|
2067
|
+
}, MainPanelPage);
|
|
2068
|
+
});
|
|
2069
|
+
var rowDispose = ctx.slots.inject("sidebar.panellist", function () {
|
|
2070
|
+
return ctx.slots.register({
|
|
2071
|
+
name: "sidebar.panellist",
|
|
2072
|
+
id: NW_PANEL_ID,
|
|
2073
|
+
order: NW_PANEL_ORDER,
|
|
2074
|
+
label: function () { return t("panel.title"); }
|
|
2075
|
+
}, SidebarPanelIcon);
|
|
2076
|
+
});
|
|
2077
|
+
if (typeof rowDispose === "function") disposers.push(rowDispose);
|
|
2078
|
+
if (typeof pageDispose === "function") disposers.push(pageDispose);
|
|
2079
|
+
return true;
|
|
2080
|
+
} catch (error) {
|
|
2081
|
+
console.warn("[dsh-novel-writer] official panel seat failed:", error);
|
|
2082
|
+
if (typeof pageDispose === "function") { try { pageDispose(); } catch { /* ignore */ } }
|
|
2083
|
+
return false;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
|
|
1946
2087
|
// ---- 插件入口 ----
|
|
1947
2088
|
var inject = ["slots", "locale"];
|
|
1948
2089
|
var applied = false;
|
|
@@ -2087,15 +2228,51 @@ var TOOL_GROUPS = [
|
|
|
2087
2228
|
} catch { /* ignore */ }
|
|
2088
2229
|
};
|
|
2089
2230
|
controller.setToggle(toggle);
|
|
2090
|
-
|
|
2231
|
+
// v5.2.0:入口与面板都走官方席位(侧栏一行 + 主栏页面);拿不到就回退 v5.1.1 的 DOM 注入。
|
|
2232
|
+
var officialPanel = false;
|
|
2233
|
+
var openPanel = function () {
|
|
2091
2234
|
controller.set({ panelOpen: !controller.getSnapshot().panelOpen }, { silent: true });
|
|
2092
2235
|
};
|
|
2093
2236
|
var disposers = [];
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
console.warn("[dsh-novel-writer] UI mount failed:", error);
|
|
2237
|
+
var legacyEntryDispose = null;
|
|
2238
|
+
var legacyPanelDispose = null;
|
|
2239
|
+
var mountLegacyEntry = function () {
|
|
2240
|
+
if (legacyEntryDispose) return;
|
|
2241
|
+
try { legacyEntryDispose = mountSidebarEntry(controller, openPanel) || null; } catch (error) { console.warn("[dsh-novel-writer] UI mount failed:", error); }
|
|
2242
|
+
};
|
|
2243
|
+
var mountLegacyPanel = function () {
|
|
2244
|
+
if (legacyPanelDispose) return;
|
|
2245
|
+
try { legacyPanelDispose = mountPanel(controller, toggle) || null; } catch (error) { console.warn("[dsh-novel-writer] UI mount failed:", error); }
|
|
2246
|
+
};
|
|
2247
|
+
var unmountLegacyEntry = function () {
|
|
2248
|
+
if (!legacyEntryDispose) return;
|
|
2249
|
+
try { legacyEntryDispose(); } catch { /* ignore */ }
|
|
2250
|
+
legacyEntryDispose = null;
|
|
2251
|
+
};
|
|
2252
|
+
var unmountLegacyPanel = function () {
|
|
2253
|
+
if (!legacyPanelDispose) return;
|
|
2254
|
+
try { legacyPanelDispose(); } catch { /* ignore */ }
|
|
2255
|
+
legacyPanelDispose = null;
|
|
2256
|
+
};
|
|
2257
|
+
/** 接管官方席位;成功则撤掉 DOM 注入的旧入口与旧面板。 */
|
|
2258
|
+
var tryOfficialPanel = function () {
|
|
2259
|
+
if (officialPanel) return true;
|
|
2260
|
+
if (!registerOfficialPanel(ctx, controller, toggle, disposers)) return false;
|
|
2261
|
+
officialPanel = true;
|
|
2262
|
+
unmountLegacyEntry();
|
|
2263
|
+
unmountLegacyPanel();
|
|
2264
|
+
return true;
|
|
2265
|
+
};
|
|
2266
|
+
mountLegacyEntry();
|
|
2267
|
+
mountLegacyPanel();
|
|
2268
|
+
exports.__internals.tryOfficialPanel = tryOfficialPanel; // 供测试直接驱动
|
|
2269
|
+
if (!tryOfficialPanel() && typeof ctx.slots.subscribe === "function") {
|
|
2270
|
+
// 席位由 shell 包声明,可能晚于我们加载:slots.inject 的回调会在声明后触发,
|
|
2271
|
+
// 这里再订一次槽位变化兜底(两条路都只读槽位,不参与宿主激活判定)。
|
|
2272
|
+
try {
|
|
2273
|
+
var stopWatch = ctx.slots.subscribe("sidebar.panellist", function () { tryOfficialPanel(); });
|
|
2274
|
+
if (typeof stopWatch === "function") disposers.push(stopWatch);
|
|
2275
|
+
} catch { /* 订阅不可用:slots.inject 的回调仍会在席位声明后触发 */ }
|
|
2099
2276
|
}
|
|
2100
2277
|
// 官方设置页「插件配置」卡片(v0.4.0 合并;slots 服务由 inject 声明)
|
|
2101
2278
|
// 注(v3.9.5):settings.plugin.item 是 keyed slot——宿主需在其 settingsScope served 集合中登记
|
package/lib/core.js
CHANGED
|
@@ -1138,12 +1138,24 @@ export async function readJsonBody(req) {
|
|
|
1138
1138
|
}
|
|
1139
1139
|
try { return JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch { return void 0; }
|
|
1140
1140
|
}
|
|
1141
|
+
/**
|
|
1142
|
+
* v5.2.0 修正:`explorer.exe` 的退出码**不可信**——它成功打开目录时也常返回 1(Windows 的既有行为),
|
|
1143
|
+
* 于是 `resolve(!err)` 把成功判成了失败:面板上显示「explorer 启动失败,未能定位目录」,而目录其实已经打开了。
|
|
1144
|
+
* 判据改为只认"进程根本没起来"——那种错误的 `code` 是字符串 errno(ENOENT / EACCES …);
|
|
1145
|
+
* 带数字 `code`(退出码)说明进程确实跑过,一律视为已交给系统外壳。
|
|
1146
|
+
* @param err - execFile 回调的错误对象(成功时为 null)。
|
|
1147
|
+
* @returns 是否属于"连启动都没起来"的失败。
|
|
1148
|
+
*/
|
|
1149
|
+
export function explorerSpawnFailed(err) {
|
|
1150
|
+
if (!err) return false;
|
|
1151
|
+
return typeof err.code !== "number";
|
|
1152
|
+
}
|
|
1141
1153
|
export async function openInExplorer(dir) {
|
|
1142
1154
|
if (process.platform === "win32") {
|
|
1143
|
-
// execFile 数组直调不经 shell,路径含 &
|
|
1155
|
+
// execFile 数组直调不经 shell,路径含 & 等特殊字符不会注入
|
|
1144
1156
|
return await new Promise(function (resolve) {
|
|
1145
1157
|
try {
|
|
1146
|
-
execFile("explorer.exe", ["/select," + dir], function (err) { resolve(!err); });
|
|
1158
|
+
execFile("explorer.exe", ["/select," + dir], function (err) { resolve(!explorerSpawnFailed(err)); });
|
|
1147
1159
|
} catch { resolve(false); }
|
|
1148
1160
|
});
|
|
1149
1161
|
} else if (process.platform === "darwin") {
|
|
@@ -1324,7 +1336,7 @@ export function makeStateRoutes(allowLan, config) {
|
|
|
1324
1336
|
} catch { /* 目录已存在或创建失败都不阻塞打开 */ }
|
|
1325
1337
|
try {
|
|
1326
1338
|
const opened = await openInExplorer(dir);
|
|
1327
|
-
if (!opened) return writeJson(res, 500, { ok: false, error: "explorer
|
|
1339
|
+
if (!opened) return writeJson(res, 500, { ok: false, error: "未能调用系统文件管理器(explorer / open / xdg-open 都没能启动);可手动打开:" + dir });
|
|
1328
1340
|
return writeJson(res, 200, { ok: true, path: dir });
|
|
1329
1341
|
} catch (error) {
|
|
1330
1342
|
return writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-novel-writer",
|
|
3
|
-
"version": "5.
|
|
4
|
-
"description": "小说写作助手插件(v5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
|
+
"description": "小说写作助手插件(v5.2.0):句式/情感/意象分析、伏笔设定管理、本地语义检索、氛围光谱、风格画像报告、文笔六维基线带、原创模式与创作资料、写作哨兵(衔接/OOC/大纲);侧边栏面板走 DSH 0.2.0 官方席位(右栏 tab + 左栏入口);附 stdio MCP 服务器(Claude Desktop / Cursor 可用)。",
|
|
5
5
|
"mcpName": "io.github.siweina/dsh-novel-writer",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"type": "git",
|
|
73
73
|
"url": "git+https://github.com/siweina/dsh-novel-writer.git"
|
|
74
74
|
},
|
|
75
|
-
"homepage": "https://github.
|
|
75
|
+
"homepage": "https://siweina.github.io/dsh-novel-writer/",
|
|
76
76
|
"bugs": {
|
|
77
77
|
"url": "https://github.com/siweina/dsh-novel-writer/issues"
|
|
78
78
|
},
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.siweina/dsh-novel-writer",
|
|
4
4
|
"title": "dsh-novel-writer — 网文写作体检",
|
|
5
5
|
"description": "Offline toolkit: 18 tools for Chinese web-novel style, plot-thread and continuity audits.",
|
|
6
|
-
"version": "5.1.
|
|
6
|
+
"version": "5.1.1",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/siweina/dsh-novel-writer",
|
|
9
9
|
"source": "github"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "dsh-novel-writer",
|
|
16
|
-
"version": "5.1.
|
|
16
|
+
"version": "5.1.1",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|