dsh-novel-writer 3.2.1 → 3.5.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/LICENSE +21 -0
- package/README.en.md +3 -3
- package/README.md +3 -3
- package/cordis.patch.yml +1 -1
- package/lib/analysis.js +20 -6
- package/lib/client.js +68 -25
- package/lib/embedding.js +11 -2
- package/lib/index.js +215 -82
- package/lib/lexicons/markers.js +4 -4
- package/lib/style-metrics.js +24 -5
- package/lib/update-check.js +6 -2
- package/lib/vibe.js +20 -12
- package/package.json +3 -3
- package/skills/novel-writing/SKILL.md +14 -11
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 siweina
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
CHANGED
|
@@ -8,7 +8,7 @@ English | [**中文**](./README.md)
|
|
|
8
8
|
[](https://github.com/siweina/dsh-novel-writer/releases)
|
|
9
9
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
10
10
|
|
|
11
|
-
A novel-writing assistant plugin for **DeepSeek Harness (DSH)
|
|
11
|
+
A novel-writing assistant plugin for **DeepSeek Harness (DSH)**: chapter library management, sentence-pattern analysis, emotion purification & quantification, **12-axis vibe spectrum**, **style portrait report**, **six-dimension style baseline band**, **writing sentinels (bridge/OOC/outline drift)**, plot & settings management, local semantic search (0 token), webnovel signal detection, **original mode with creation files (bible/dynamic outline/hook backfill)**, batch import, and AI-assisted continuation writing. The semantic engine depends on `onnxruntime-web` and `@huggingface/tokenizers` (auto-installed with the package). **Requires Node ≥ 22.3.**
|
|
12
12
|
|
|
13
13
|
> **A note to non-Chinese users**: This plugin is designed specifically for Chinese-language novel analysis and writing — its core capabilities (sentence-pattern analysis, emotion quantification, imagery detection) and its built-in semantic model are all built and tuned for Chinese text. Fully supporting English or other languages alongside Chinese is beyond my current capability. I sincerely apologize for any inconvenience this may cause.
|
|
14
14
|
|
|
@@ -39,14 +39,14 @@ After installing, **restart the web app** to activate (host registers 16 tools +
|
|
|
39
39
|
|
|
40
40
|
1. **Style portrait report** (`novel_style_report`): 6-dimension measurement — style fingerprint / high-frequency lexicon / genre-theme / emotion quantification / 12-axis vibe spectrum / semantic style distance. **Measurement-judgment separation**: the plugin only reports numbers, never labels; AI judgment can be saved back to `.novel-writer/style-reports/` for consistent continuation writing.
|
|
41
41
|
2. **12-axis vibe spectrum**: nightmare / angst / heartwarming / fluff / tearjerker / dark / mystery / blaze / absurd / lonesome / aesthetic / sensual — with traceable evidence, 0 token.
|
|
42
|
-
3. **Local semantic engine**: bge-small-zh Chinese model (
|
|
42
|
+
3. **Local semantic engine**: bge-small-zh Chinese model (24MB, shipped with the plugin) local CPU inference — `novel_semantic_search` finds semantically related passages with natural language (with chapter location), semantic style comparison, semantic implicit emotion; lazy loading + graceful fallback.
|
|
43
43
|
4. **Sentence-pattern analysis**: 9 categories, arrangement patterns, rhythm, emotion curve, style fingerprint + guidance, with cache & report export.
|
|
44
44
|
5. **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.
|
|
45
45
|
6. **Worldview & pragmatics detection**: auto cultural-baseline detection with confidence; speechStyle title/honorifics/rituals/tone norms; genre & theme + webnovel signals.
|
|
46
46
|
7. **Writing toolkit**: plot tracking / five settings tables (characters·locations·items·timeline·worldview) / chapter summaries / continuity audit / batch import / style check / continuation writing.
|
|
47
47
|
8. **Per-tool UI toggles**: "Writing Assistant" sidebar panel (master + grouped tool toggles + feature toggles), plain-language labels, data-dir usage & semantic-engine status display.
|
|
48
48
|
9. **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.
|
|
49
|
-
10. **Writing sentinels
|
|
49
|
+
10. **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.
|
|
50
50
|
11. **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.
|
|
51
51
|
12. **Experience & stats**: main panel **library stats** (per-book chapters/total chars/7-day active chars, 🔥 green), **🎬 demo** (built-in sample, no files, runs the 6-dim baseline), **📊 report history** (analysis/style-reports browsing); actionable error hints; slimmer tool descriptions.
|
|
52
52
|
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](https://github.com/siweina/dsh-novel-writer/releases)
|
|
9
9
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
10
10
|
|
|
11
|
-
为 **DeepSeek Harness (DSH)**
|
|
11
|
+
为 **DeepSeek Harness (DSH)** 打造的小说写作助手插件:章节库管理、句式分析、情感净化与量化、氛围光谱、**风格画像报告**、**文笔六维基线带**、**写作哨兵三件套(衔接/OOC/大纲走偏)**、伏笔设定管理、本地语义检索(0 token)、网文信号识别、**原创模式与创作资料管理(设定书/动态大纲/钩子回填)**、批量导入与 AI 续写辅助。语义引擎依赖 `onnxruntime-web` 与 `@huggingface/tokenizers`(随包自动安装),浏览器端仅依赖 Web GUI 自带的 react。**要求 Node ≥ 22.3。**
|
|
12
12
|
|
|
13
13
|
> **致非中文用户**:本插件为中文小说分析写作而设计——句式、情感、意象等核心能力以及内置的语义模型,全部针对中文语料构建与调优。在深耕中文的同时兼顾英文等其他语言,确实超出了我目前的能力范围。若因此给您带来不便,我深感抱歉,恳请谅解。
|
|
14
14
|
|
|
@@ -39,14 +39,14 @@ dsh plugin --profile web add github:siweina/dsh-novel-writer#main
|
|
|
39
39
|
|
|
40
40
|
1. **风格画像报告**(novel_style_report):6 维测量报告——文风指纹 / 高频词汇 / 题材流派 / 情感量化 / 氛围光谱 12 轴 / 语义风格距离。**测量与判断分离**:插件只报数不贴标签,AI 判断可回传存盘(`.novel-writer/style-reports/`),续写保持风格一致。
|
|
41
41
|
2. **氛围光谱 12 轴**:噩梦感 / 焦虑压抑 / 温馨治愈 / 甜宠日常 / 催泪虐心 / 黑暗残酷 / 悬疑神秘 / 热血激昂 / 荒诞无厘头 / 孤独疏离 / 文艺唯美 / 情欲暧昧——证据链可追溯,0 token。
|
|
42
|
-
3. **本地语义引擎**:bge-small-zh 中文模型(
|
|
42
|
+
3. **本地语义引擎**:bge-small-zh 中文模型(24MB 随插件分发)本地 CPU 推理——`novel_semantic_search` 自然语言搜全书语义相关段落(带章节定位),语义级风格对比、语义隐性情感,懒加载 + 自动回退。
|
|
43
43
|
4. **句式模式分析**:九类句式分布、排列规律、句长节奏、情感曲线、风格指纹与节奏建议,带缓存与报告导出。
|
|
44
44
|
5. **情感净化 + 量化**:强/弱情绪词分级、污染源检测、caveat 预警 + AI 复核;Valence 滑动窗口 → 方差 V / 斜率 Δ / 矛盾指数 C + 隐性意象载体。
|
|
45
45
|
6. **世界观与语用检测**:文化基准自动判断(西/东/混合)+ 置信度;speechStyle 称谓/客套/仪式/语气规范;题材流派 + 网文信号。
|
|
46
46
|
7. **写作辅助全家桶**:伏笔登记表 / 设定五张表(人物·地点·道具·时间线·世界观)/ 章节摘要 / 连贯性审计 / 批量导入 / 风格自检 / 续写辅助。
|
|
47
47
|
8. **全工具 UI 开关**:侧边栏「写作助手功能」面板(总开关 + 工具开关分组 + 功能开关),大白话文案,显示数据目录占用与语义引擎状态。
|
|
48
48
|
9. **风格基线**:文笔六维测量(句法复杂度/修饰密度/抽象度/动作密度/不确定性/留白指数)+ 按章节 μ±σ 基线带;`novel_style_report` 输出基线带,`novel_style_check` 对照新章偏差(带内 ✓ / 出带 ⚠);侧边栏可自定义每维 ±% 容差(**推荐值 = 原著章节波动的 1.5 倍 σ**,自动取整、限 ±10%~100%;输入框留空即用推荐)——原创/续写时主题自由、写法保持在基线带内。
|
|
49
|
-
10.
|
|
49
|
+
10. **写作哨兵三件套**:`novel_continuity_check` 扩展——①**衔接检查**(chapter 参数:时间硬跳/语义距离/人物延续/钩子承接四路检测,带原文引用)②**OOC 检测**(ooc 参数:角色情绪基线偏离)③**大纲走偏**(outline 参数:方向行 vs 正文关键词重合);报告工具支持 **brief 精简模式**。
|
|
50
50
|
11. **原创模式与创作资料**:侧边栏填写创作设定(世界观/角色/禁忌/主线/题材/额外要求,留空=模型自定,多书独立设定库);novel_outline 维护创作资料(创作设定/人物/剧情大纲/钩子记录/创作状态卡),原创强制「设定书→大纲→钩子」链,动态批次(10→20→30 章)防剧情跳跃与角色 OOC。
|
|
51
51
|
12. **体验与统计**:主面板**书库统计卡**(每本章数/总字数/近 7 天活跃字数,活跃🔥标绿)、**🎬 体验演示**(内置示例不落盘跑六维基线)、**📊 报告历史**(analysis/style-reports 列表浏览);错误提示带解决步骤;工具说明压缩省 token。
|
|
52
52
|
|
package/cordis.patch.yml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# dsh-novel-writer
|
|
1
|
+
# dsh-novel-writer v3.5.0 bundle patch: 16 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.
|
package/lib/analysis.js
CHANGED
|
@@ -131,6 +131,8 @@ export function splitBlocks(text) {
|
|
|
131
131
|
let current = [];
|
|
132
132
|
for (const line of lines) {
|
|
133
133
|
const trimmed = line.trim();
|
|
134
|
+
// v3.5.0 #51:Markdown 标题(# 开头)不是句子——跳过(含空行场景)
|
|
135
|
+
if (/^#{1,6}\s/.test(trimmed)) continue;
|
|
134
136
|
if (trimmed === "") {
|
|
135
137
|
if (current.length > 0) {
|
|
136
138
|
blocks.push(current.join("\n").trim());
|
|
@@ -154,6 +156,13 @@ export function splitSentences(block) {
|
|
|
154
156
|
let buffer = "";
|
|
155
157
|
for (let i = 0; i < block.length; i += 1) {
|
|
156
158
|
const ch = block[i];
|
|
159
|
+
// v3.5.0 #50b:无标点换行也是句边界(逐行排版正确分句,整篇不再算 1 句)
|
|
160
|
+
if (ch === "\n" || ch === "\r") {
|
|
161
|
+
const lineSentence = buffer.trim();
|
|
162
|
+
if (lineSentence !== "") sentences.push(lineSentence);
|
|
163
|
+
buffer = "";
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
157
166
|
buffer += ch;
|
|
158
167
|
if (TERMINATORS.includes(ch)) {
|
|
159
168
|
while (i + 1 < block.length && TERMINATORS.includes(block[i + 1])) {
|
|
@@ -267,7 +276,7 @@ export function classifySentence(raw) {
|
|
|
267
276
|
/** 单字情感词的多字搭配排除("叹气"不算"气"、"笑道"不算"笑")。 */
|
|
268
277
|
const EMOTION_EXCLUDE = {
|
|
269
278
|
joy: { "笑": ["笑道", "说笑", "玩笑", "搞笑", "笑话", "苦笑", "冷笑", "嘲笑", "嬉笑", "傻笑", "微笑", "笑死", "笑呵呵", "笑眯眯"] },
|
|
270
|
-
anger: { "气": ["叹气", "运气", "客气", "一口气", "服气", "脾气", "争气", "节气", "淘气", "景气", "暖气", "口气", "名气", "勇气", "底气", "俗气", "风气", "香气", "水气", "湿气", "潮气", "和气", "生气勃勃"] },
|
|
279
|
+
anger: { "气": ["叹气", "运气", "客气", "一口气", "服气", "脾气", "争气", "节气", "淘气", "景气", "暖气", "口气", "名气", "勇气", "底气", "俗气", "风气", "香气", "水气", "湿气", "潮气", "和气", "生气勃勃"], "生气": ["生气勃勃"] },
|
|
271
280
|
sorrow: { "悲": ["慈悲"], "哭": ["哭丧"] },
|
|
272
281
|
fear: { "怕": ["哪怕", "只怕", "生怕"] },
|
|
273
282
|
surprise: { "愣": ["愣住"] }
|
|
@@ -296,10 +305,12 @@ function emotionOf(text) {
|
|
|
296
305
|
// 单字词的搭配排除(如"叹气"中的"气")
|
|
297
306
|
const exclude = EMOTION_EXCLUDE[emotion]?.[word];
|
|
298
307
|
if (exclude !== void 0) {
|
|
308
|
+
// v3.5.0 #54:窗口按最长排除词取("生气勃勃"=4 字,旧窗口永远命不中)
|
|
309
|
+
const maxLen = Math.max(4, ...exclude.map(function (x) { return x.length; }));
|
|
299
310
|
const two = text.slice(Math.max(0, idx - 1), idx + 2);
|
|
300
311
|
const prevTwo = text.slice(Math.max(0, idx - 1), idx + 1);
|
|
301
312
|
const nextTwo = text.slice(idx, idx + 2);
|
|
302
|
-
const prevFour = text.slice(Math.max(0, idx -
|
|
313
|
+
const prevFour = text.slice(Math.max(0, idx - maxLen), idx + maxLen);
|
|
303
314
|
let excluded = false;
|
|
304
315
|
for (const bad of exclude) {
|
|
305
316
|
if (two.includes(bad) || prevTwo === bad || nextTwo === bad || prevFour.includes(bad)) {
|
|
@@ -682,7 +693,8 @@ export function valenceSeries(text, winChars = 100) {
|
|
|
682
693
|
|
|
683
694
|
/** v1.6.0 三指标:方差 V + 相邻撕裂 V_adj + 斜率 Δ(最小二乘+鲁棒版)+ 矛盾指数 C。 */
|
|
684
695
|
export function valenceStats(text) {
|
|
685
|
-
|
|
696
|
+
// v3.5.0 #57:一次 valenceSeries 取全部(旧版窗口统计二次调用,大书白扫一遍词表)
|
|
697
|
+
const { series, posWords, negWords, windowPosNeg } = valenceSeries(text);
|
|
686
698
|
const n = series.length;
|
|
687
699
|
if (n === 0) return { windows: 0 };
|
|
688
700
|
const mean = series.reduce((x, y) => x + y, 0) / n;
|
|
@@ -701,7 +713,7 @@ export function valenceStats(text) {
|
|
|
701
713
|
const third = Math.max(1, Math.floor(n / 3));
|
|
702
714
|
const headMean = series.slice(0, third).reduce((x, y) => x + y, 0) / third;
|
|
703
715
|
const tailMean = series.slice(-third).reduce((x, y) => x + y, 0) / third;
|
|
704
|
-
|
|
716
|
+
// v3.5.0 #57:复用上面的 windowPosNeg(不再二次扫描)
|
|
705
717
|
const totalWords = posWords + negWords;
|
|
706
718
|
const posRatio = totalWords === 0 ? 0 : posWords / totalWords;
|
|
707
719
|
const negRatio = totalWords === 0 ? 0 : negWords / totalWords;
|
|
@@ -856,7 +868,8 @@ export function emotionalQuantification(text, perChapter, blocks, semResolver =
|
|
|
856
868
|
*/
|
|
857
869
|
export function analyzeText(text, options = {}) {
|
|
858
870
|
const top = Number.isInteger(options.top) ? options.top : 8;
|
|
859
|
-
|
|
871
|
+
// v3.5.0 #63:maxSentences 钳制 ≥1(0/负数会让 slice(0,-5) 产生错误语义)
|
|
872
|
+
const maxSentences = Math.max(1, Number.isInteger(options.maxSentences) ? options.maxSentences : 20000);
|
|
860
873
|
const curveSegments = Number.isInteger(options.curveSegments) ? Math.min(Math.max(options.curveSegments, 1), 50) : 20;
|
|
861
874
|
const blocks = splitBlocks(text);
|
|
862
875
|
const sentences = [];
|
|
@@ -1269,7 +1282,8 @@ const DENSITY_SENSE = {
|
|
|
1269
1282
|
/** 细节密度统计(v0.8.0):动作链/物件名词/感官词,按千字归一。 */
|
|
1270
1283
|
export function densityOf(text) {
|
|
1271
1284
|
const sentences = splitSentences(text);
|
|
1272
|
-
|
|
1285
|
+
// v3.5.0 #62:分母口径统一(含空白,与 style-metrics 的 per1000 一致,跨模块数值可比)
|
|
1286
|
+
const totalChars = String(text).length;
|
|
1273
1287
|
let actionVerbs = 0;
|
|
1274
1288
|
let actionChainSentences = 0;
|
|
1275
1289
|
let objectHits = 0;
|
package/lib/client.js
CHANGED
|
@@ -14,6 +14,8 @@ window.__ModuleLoader__.load({
|
|
|
14
14
|
id: "dsh-novel-writer",
|
|
15
15
|
factory: (require) => {
|
|
16
16
|
var module = { exports: {} };
|
|
17
|
+
// v3.5.0 R1:rawTimer 必须在 createController 闭包外/顶部声明(旧位置在渲染函数内,api 方法访问不到 → ReferenceError)
|
|
18
|
+
var rawTimer = null;
|
|
17
19
|
var exports = module.exports;
|
|
18
20
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
19
21
|
let react = require("react");
|
|
@@ -30,7 +32,7 @@ var css = `
|
|
|
30
32
|
html[data-dsh-novel-writer-active]:not([data-dsh-ssh-active]):not([data-dsh-taskboard-active]) [class*=centerCol]>:not([data-dsh-novel-writer-view]){display:none!important}
|
|
31
33
|
[data-dsh-frame][data-sidebar-collapsed] .nwEntry{justify-content:center;width:100%;padding:0}
|
|
32
34
|
[data-dsh-frame][data-sidebar-collapsed] .nwEntryLabel{display:none}
|
|
33
|
-
.nwSectionTitle{.nwTolVer{font-size:10px;color:var(--dsw-alias-label-tertiary,#94a3b8);margin:-2px 0 2px;letter-spacing:.03em}.nwTolCard{background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e2e8f0);border-radius:12px;padding:4px 14px 8px;margin:8px 0 4px;box-shadow:0 1px 4px rgba(15,23,42,.05)}.nwTolRow{display:flex;align-items:center;gap:10px;padding:9px 2px}.nwTolRow+.nwTolRow{border-top:1px dashed var(--dsw-alias-border-l2,#eef2f7)}.nwTolName{flex:1;min-width:0;font-size:13px;color:var(--dsw-alias-label-primary,#334155);font-weight:500;display:flex;align-items:center;gap:8px;white-space:nowrap}.nwTolIcon{font-size:15px;opacity:.9;flex-shrink:0}.nwTolField{width:66px;flex-shrink:0;display:flex;flex-direction:column;align-items:stretch;gap:3px}.nwTolInput{width:100%;height:30px;padding:0 4px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,#d4d4d8);border-radius:8px;font-size:13px;text-align:center;color:var(--dsw-alias-label-primary,#18181b);background:var(--dsw-alias-bg-layer-2,#fafafa);transition:border-color .18s,box-shadow .18s,background .18s;-moz-appearance:textfield}.nwTolInput::-webkit-outer-spin-button,.nwTolInput::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}.nwTolInput:hover{border-color:#a1a1aa}.nwTolInput:focus{outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.16);background:#fff}.nwTolInput::placeholder{color:#c4c8d0;font-size:11px}.nwTolCaption{font-size:10px;color:var(--dsw-alias-label-tertiary,#94a3b8);text-align:center}.nwTolSep{width:14px;flex-shrink:0;font-size:12px;color:var(--dsw-alias-label-tertiary,#b0b8c4);text-align:center}.nwTolPct{width:14px;flex-shrink:0;font-size:13px;color:var(--dsw-alias-label-secondary,#64748b);text-align:left}.nwTolBtns{display:flex;gap:12px;margin-top:16px}.nwBtnPrimary{background:linear-gradient(135deg,#6366f1,#8b5cf6);border:none;color:#fff;font-weight:600;font-size:13px;padding:9px 20px;border-radius:10px;cursor:pointer;box-shadow:0 2px 6px rgba(99,102,241,.28);transition:transform .15s,box-shadow .15s,opacity .15s}.nwBtnPrimary:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(99,102,241,.38)}.nwBtnPrimary:active{transform:translateY(0);box-shadow:0 1px 3px rgba(99,102,241,.3)}.nwBtnDone{background:linear-gradient(135deg,#10b981,#34d399);box-shadow:0 2px 6px rgba(16,185,129,.3)}.nwBtnGhost{background:transparent;border:1px solid var(--dsw-alias-border-l2,#cbd5e1);color:var(--dsw-alias-label-secondary,#475569);font-size:13px;padding:9px 18px;border-radius:10px;cursor:pointer;transition:background .15s,border-color .15s,transform .15s}.nwBtnGhost:hover{background:var(--dsw-alias-bg-layer-2,#f1f5f9);border-color:#a1a1aa;transform:translateY(-1px)}.nwUpdateBar{display:block;margin:4px 0 8px;padding:7px 10px;border-radius:6px;background:#e8f5e9;color:#1b5e20;font-size:12px;text-decoration:none;border:1px solid #a5d6a7;cursor:pointer}.nwUpdateBar:hover{background:#c8e6c9}
|
|
35
|
+
.nwSectionTitle{display:flex;align-items:center;gap:6px;font-weight:700;font-size:13px;margin-top:6px}.nwTolVer{font-size:10px;color:var(--dsw-alias-label-tertiary,#94a3b8);margin:-2px 0 2px;letter-spacing:.03em}.nwTolCard{background:var(--dsw-alias-bg-layer-1,#fff);border:1px solid var(--dsw-alias-border-l2,#e2e8f0);border-radius:12px;padding:4px 14px 8px;margin:8px 0 4px;box-shadow:0 1px 4px rgba(15,23,42,.05)}.nwTolRow{display:flex;align-items:center;gap:10px;padding:9px 2px}.nwTolRow+.nwTolRow{border-top:1px dashed var(--dsw-alias-border-l2,#eef2f7)}.nwTolName{flex:1;min-width:0;font-size:13px;color:var(--dsw-alias-label-primary,#334155);font-weight:500;display:flex;align-items:center;gap:8px;white-space:nowrap}.nwTolIcon{font-size:15px;opacity:.9;flex-shrink:0}.nwTolField{width:66px;flex-shrink:0;display:flex;flex-direction:column;align-items:stretch;gap:3px}.nwTolInput{width:100%;height:30px;padding:0 4px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2,#d4d4d8);border-radius:8px;font-size:13px;text-align:center;color:var(--dsw-alias-label-primary,#18181b);background:var(--dsw-alias-bg-layer-2,#fafafa);transition:border-color .18s,box-shadow .18s,background .18s;-moz-appearance:textfield}.nwTolInput::-webkit-outer-spin-button,.nwTolInput::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}.nwTolInput:hover{border-color:#a1a1aa}.nwTolInput:focus{outline:none;border-color:#6366f1;box-shadow:0 0 0 3px rgba(99,102,241,.16);background:#fff}.nwTolInput::placeholder{color:#c4c8d0;font-size:11px}.nwTolCaption{font-size:10px;color:var(--dsw-alias-label-tertiary,#94a3b8);text-align:center}.nwTolSep{width:14px;flex-shrink:0;font-size:12px;color:var(--dsw-alias-label-tertiary,#b0b8c4);text-align:center}.nwTolPct{width:14px;flex-shrink:0;font-size:13px;color:var(--dsw-alias-label-secondary,#64748b);text-align:left}.nwTolBtns{display:flex;gap:12px;margin-top:16px}.nwBtnPrimary{background:linear-gradient(135deg,#6366f1,#8b5cf6);border:none;color:#fff;font-weight:600;font-size:13px;padding:9px 20px;border-radius:10px;cursor:pointer;box-shadow:0 2px 6px rgba(99,102,241,.28);transition:transform .15s,box-shadow .15s,opacity .15s}.nwBtnPrimary:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(99,102,241,.38)}.nwBtnPrimary:active{transform:translateY(0);box-shadow:0 1px 3px rgba(99,102,241,.3)}.nwBtnDone{background:linear-gradient(135deg,#10b981,#34d399);box-shadow:0 2px 6px rgba(16,185,129,.3)}.nwBtnGhost{background:transparent;border:1px solid var(--dsw-alias-border-l2,#cbd5e1);color:var(--dsw-alias-label-secondary,#475569);font-size:13px;padding:9px 18px;border-radius:10px;cursor:pointer;transition:background .15s,border-color .15s,transform .15s}.nwBtnGhost:hover{background:var(--dsw-alias-bg-layer-2,#f1f5f9);border-color:#a1a1aa;transform:translateY(-1px)}.nwUpdateBar{display:block;margin:4px 0 8px;padding:7px 10px;border-radius:6px;background:#e8f5e9;color:#1b5e20;font-size:12px;text-decoration:none;border:1px solid #a5d6a7;cursor:pointer}.nwUpdateBar:hover{background:#c8e6c9}
|
|
34
36
|
.nwNavEntry{display:flex;align-items:center;justify-content:space-between;width:100%;box-sizing:border-box;padding:8px 10px;margin:4px 0;border:1px solid var(--dsw-alias-border);border-radius:8px;background:var(--dsw-alias-surface-1);cursor:pointer;text-align:left;color:inherit;font:inherit}
|
|
35
37
|
.nwNavEntry:hover{border-color:#22c55e}
|
|
36
38
|
.nwNavEntryText{flex:1;min-width:0;padding-right:8px}
|
|
@@ -231,6 +233,7 @@ var css = `
|
|
|
231
233
|
"toolGroup.create.desc": "新建章节与调整开关配置。",
|
|
232
234
|
"group.allOn": "全部开启",
|
|
233
235
|
"group.allOff": "全部关闭",
|
|
236
|
+
"group.onCount": "{n} 开",
|
|
234
237
|
"group.partial": "部分开启(点一下全部开启)",
|
|
235
238
|
"panel.toolsHint": "全部默认开启。关闭后 AI 调用该工具会收到明确提示;可随时在此重新开启。",
|
|
236
239
|
"panel.loading": "正在读取开关状态…",
|
|
@@ -333,6 +336,7 @@ var css = `
|
|
|
333
336
|
"creation.deleted": "已删除该书设定",
|
|
334
337
|
"creation.newed": "已创建设定,开始填写",
|
|
335
338
|
"creation.needName": "请先输入新书书名",
|
|
339
|
+
"stats.pending": "统计未就绪(宿主未提供,点刷新重试)",
|
|
336
340
|
"stats.empty": "书库为空——在 novels/ 放一本小说,或让 AI 原创一本",
|
|
337
341
|
"stats.title": "书库统计",
|
|
338
342
|
"stats.week": "7天",
|
|
@@ -358,6 +362,7 @@ var css = `
|
|
|
358
362
|
var en = {
|
|
359
363
|
"entry.label": "Writing Assistant",
|
|
360
364
|
"entry.tooltip": "Writing assistant switches (dsh-novel-writer): analysis / style check / plot tracking etc.",
|
|
365
|
+
"panel.back": "Back",
|
|
361
366
|
"panel.title": "Writing Assistant",
|
|
362
367
|
"panel.desc": "Manage the writing-assistant features: sentence-pattern analysis, style check, plot tracking, import, etc. Disabled tools return a clear notice when called.",
|
|
363
368
|
"panel.enabled": "Enable writing assistant",
|
|
@@ -413,7 +418,11 @@ var css = `
|
|
|
413
418
|
"toolGroup.analyze": "📊 Analysis",
|
|
414
419
|
"toolGroup.settings": "📚 Settings",
|
|
415
420
|
"toolGroup.create": "✍️ Creation",
|
|
416
|
-
|
|
421
|
+
"group.allOn": "All on",
|
|
422
|
+
"group.allOff": "All off",
|
|
423
|
+
"group.onCount": "{n} on",
|
|
424
|
+
"group.partial": "Partially on (click to toggle all)",
|
|
425
|
+
"panel.toolsHint": "All on by default. Disabled tools return a clear notice; re-enable anytime here.",
|
|
417
426
|
"panel.loading": "Loading switch state…",
|
|
418
427
|
"panel.saved": "Saved to the host state file",
|
|
419
428
|
"panel.localOnly": "Host unreachable; kept in this browser only (defaults may return after restart).",
|
|
@@ -513,6 +522,7 @@ var css = `
|
|
|
513
522
|
"creation.deleted": "Profile deleted",
|
|
514
523
|
"creation.newed": "Profile created, start filling",
|
|
515
524
|
"creation.needName": "Please enter a book name first",
|
|
525
|
+
"stats.pending": "Stats pending (host unavailable — press refresh)",
|
|
516
526
|
"stats.empty": "Library empty — put a novel in novels/, or ask AI to create one",
|
|
517
527
|
"stats.title": "Library stats",
|
|
518
528
|
"stats.week": "/7d",
|
|
@@ -617,6 +627,8 @@ var css = `
|
|
|
617
627
|
getSnapshot: function () { return state; },
|
|
618
628
|
set: function (patch) {
|
|
619
629
|
Object.assign(state, patch);
|
|
630
|
+
// v3.5.0 R4:每次状态变更自增版本号(竞态守卫用,对象引用比较是死代码)
|
|
631
|
+
state.rev = (state.rev || 0) + 1;
|
|
620
632
|
listeners.forEach(function (fn) { fn(state); });
|
|
621
633
|
},
|
|
622
634
|
subscribe: function (fn) {
|
|
@@ -626,22 +638,24 @@ var css = `
|
|
|
626
638
|
refresh: function () {
|
|
627
639
|
if (api.getSnapshot().refreshing) return Promise.resolve(null);
|
|
628
640
|
api.set({ refreshing: true, revealMsg: "", revealErr: false });
|
|
641
|
+
// v3.5.0 R4:记录请求发起时的版本号——返回时版本未变(用户没操作)才用远端覆盖开关/设定
|
|
642
|
+
var revAt = api.getSnapshot().rev;
|
|
629
643
|
return fetchState().then(function (remote) {
|
|
630
644
|
api.set({
|
|
631
645
|
enabled: !!remote.enabled,
|
|
632
646
|
autoAnalyze: !!remote.autoAnalyze,
|
|
633
|
-
|
|
634
|
-
|
|
647
|
+
// v3.5.0 R4:请求期间版本未变(用户没操作)才用远端覆盖开关——慢请求不覆盖用户刚切的
|
|
648
|
+
tools: (api.getSnapshot().rev === revAt ? (remote.tools || {}) : api.getSnapshot().tools || {}),
|
|
649
|
+
features: (api.getSnapshot().rev === revAt ? (remote.features || {}) : api.getSnapshot().features || {}),
|
|
635
650
|
plotsDir: remote.plotsDir || "",
|
|
636
651
|
dataDir: remote.dataDir || "",
|
|
637
652
|
dataDirSize: remote.dataDirSize || 0,
|
|
638
653
|
embeddingStatus: remote.embeddingStatus || null,
|
|
639
|
-
styleTolerance: remote.styleTolerance || null,
|
|
640
|
-
creationProfile: remote.creationProfile || null,
|
|
641
|
-
creationProfiles: remote.creationProfiles || {},
|
|
654
|
+
styleTolerance: (api.getSnapshot().rev === revAt ? (remote.styleTolerance || null) : api.getSnapshot().styleTolerance || null),
|
|
655
|
+
creationProfile: (api.getSnapshot().rev === revAt ? (remote.creationProfile || null) : api.getSnapshot().creationProfile || null),
|
|
656
|
+
creationProfiles: (api.getSnapshot().rev === revAt ? (remote.creationProfiles || {}) : api.getSnapshot().creationProfiles || {}),
|
|
642
657
|
books: remote.books || [],
|
|
643
|
-
booksStats: remote.booksStats || [],
|
|
644
|
-
booksStats: remote.booksStats || [],
|
|
658
|
+
booksStats: typeof remote.booksStats === "undefined" ? null : (remote.booksStats || []),
|
|
645
659
|
dirs: remote.dirs || null,
|
|
646
660
|
file: remote.file || "",
|
|
647
661
|
hostOk: true,
|
|
@@ -693,8 +707,8 @@ var css = `
|
|
|
693
707
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
694
708
|
navigator.clipboard.writeText(dir).then(function () { done(true); }, function () { done(false); });
|
|
695
709
|
} else {
|
|
696
|
-
//
|
|
697
|
-
done(
|
|
710
|
+
// v3.5.0 #45:非安全上下文实际没复制成功——如实报失败并给出路径
|
|
711
|
+
done(false);
|
|
698
712
|
}
|
|
699
713
|
},
|
|
700
714
|
setToggle: function (fn) { toggleRef = fn; },
|
|
@@ -702,25 +716,32 @@ var css = `
|
|
|
702
716
|
setToolGroupOpen: function (patch) { api.set({ toolGroupOpen: Object.assign({}, (this.state && this.state.toolGroupOpen) || {}, patch) }); },
|
|
703
717
|
rawToggle: function (wantOn) {
|
|
704
718
|
if (!wantOn) {
|
|
719
|
+
if (rawTimer) { clearInterval(rawTimer); rawTimer = null; }
|
|
705
720
|
if (toggleRef) toggleRef({ features: { rawWriting: false } });
|
|
706
721
|
api.set({ rawModal: false, rawCountdown: 0, rawPromiseOpen: false, rawPromiseText: "", rawPromiseOk: false });
|
|
707
722
|
return;
|
|
708
723
|
}
|
|
709
724
|
api.set({ rawModal: true, rawCountdown: 3, rawPromiseOpen: false, rawPromiseText: "", rawPromiseOk: false });
|
|
710
|
-
|
|
725
|
+
// v3.5.0 #43:timer 存模块级引用,取消/确认时统一清理(防双定时器竞争)
|
|
726
|
+
if (rawTimer) clearInterval(rawTimer);
|
|
727
|
+
rawTimer = setInterval(function () {
|
|
711
728
|
var n = api.getSnapshot().rawCountdown - 1;
|
|
712
|
-
if (n <= 0) { clearInterval(
|
|
729
|
+
if (n <= 0) { clearInterval(rawTimer); rawTimer = null; api.set({ rawCountdown: 0 }); }
|
|
713
730
|
else api.set({ rawCountdown: n });
|
|
714
731
|
}, 1000);
|
|
715
732
|
},
|
|
716
|
-
rawConfirm: function () { api.set({ rawModal: false, rawPromiseOpen: true, rawPromiseText: "", rawPromiseOk: false }); },
|
|
717
|
-
rawCancel: function () { api.set({ rawModal: false, rawCountdown: 0, rawPromiseOpen: false, rawPromiseText: "", rawPromiseOk: false }); },
|
|
733
|
+
rawConfirm: function () { if (rawTimer) { clearInterval(rawTimer); rawTimer = null; } api.set({ rawModal: false, rawPromiseOpen: true, rawPromiseText: "", rawPromiseOk: false }); },
|
|
734
|
+
rawCancel: function () { if (rawTimer) { clearInterval(rawTimer); rawTimer = null; } api.set({ rawModal: false, rawCountdown: 0, rawPromiseOpen: false, rawPromiseText: "", rawPromiseOk: false }); },
|
|
718
735
|
rawSetPromise: function (text) {
|
|
719
|
-
|
|
736
|
+
// v3.5.0 #36:按当前语言校验(中文"绝不传播" / 英文"never distribute")
|
|
737
|
+
var isEn = typeof document !== "undefined" && (document.documentElement.lang || "zh").toLowerCase().startsWith("en");
|
|
738
|
+
var kw = isEn ? "never distribute" : "绝不传播";
|
|
739
|
+
var ok = String(text || "").toLowerCase().indexOf(kw) !== -1 && String(text || "").length >= 5;
|
|
720
740
|
api.set({ rawPromiseText: text || "", rawPromiseOk: ok });
|
|
721
741
|
},
|
|
722
742
|
rawPromiseConfirm: function () {
|
|
723
743
|
if (!api.getSnapshot().rawPromiseOk) return;
|
|
744
|
+
if (rawTimer) { clearInterval(rawTimer); rawTimer = null; }
|
|
724
745
|
if (toggleRef) toggleRef({ features: { rawWriting: true } });
|
|
725
746
|
api.set({ rawPromiseOpen: false, rawPromiseText: "", rawPromiseOk: false, rawModal: false, rawCountdown: 0 });
|
|
726
747
|
}
|
|
@@ -788,7 +809,8 @@ var css = `
|
|
|
788
809
|
if (placed) rootObserver.observe(root, { childList: true, subtree: true });
|
|
789
810
|
};
|
|
790
811
|
var waitObserver = new MutationObserver(function () { tryPlace(); });
|
|
791
|
-
|
|
812
|
+
// v3.5.0 #44:observe 失败容错(不影响 mountPanel 挂载)
|
|
813
|
+
try { waitObserver.observe(document.body, { childList: true, subtree: true }); } catch (e) { /* body 不可观察时静默降级 */ }
|
|
792
814
|
var rootObserver = new MutationObserver(function () {
|
|
793
815
|
if (root === void 0 || !root.isConnected) {
|
|
794
816
|
placed = false;
|
|
@@ -843,7 +865,7 @@ var css = `
|
|
|
843
865
|
el("div", { className: "nwNavEntryHint" }, hint)
|
|
844
866
|
),
|
|
845
867
|
el("div", { className: "nwNavEntryRight" },
|
|
846
|
-
count === null ? null : el("span", { className: "nwBadge " + (count > 0 ? "nwBadgeOn" : "nwBadgeOff") },
|
|
868
|
+
count === null ? null : el("span", { className: "nwBadge " + (count > 0 ? "nwBadgeOn" : "nwBadgeOff") }, (t("group.onCount") || "{n} 开").replace("{n}", count)),
|
|
847
869
|
el("span", { className: "nwNavEntryArrow" }, "›")
|
|
848
870
|
)
|
|
849
871
|
);
|
|
@@ -1032,7 +1054,8 @@ var TOOL_GROUPS = [
|
|
|
1032
1054
|
);
|
|
1033
1055
|
} else if (view === "baseline") {
|
|
1034
1056
|
// v3.0.0:风格基线——六维文笔指标 ±% 容差带(左=允许低于,右=允许高于)
|
|
1035
|
-
|
|
1057
|
+
var DEFAULT_TOL = { low: "", high: "" };
|
|
1058
|
+
var NW_METRICS = [
|
|
1036
1059
|
["complexity", "句法复杂度", "📐"], ["modifierDensity", "修饰密度", "🎨"], ["abstractDensity", "抽象度", "☁️"],
|
|
1037
1060
|
["actionDensity", "动作密度", "⚡"], ["hedgeDensity", "不确定性", "🌫️"], ["gapIndex", "留白指数", "🕳️"]
|
|
1038
1061
|
];
|
|
@@ -1123,9 +1146,12 @@ var TOOL_GROUPS = [
|
|
|
1123
1146
|
);
|
|
1124
1147
|
} else if (view === "reports") {
|
|
1125
1148
|
// v3.2.0:报告历史(加载在入口 onClick 触发,此处纯渲染防 React 345 循环)
|
|
1149
|
+
var reportReqId = 0;
|
|
1126
1150
|
var openReport = function (file) {
|
|
1151
|
+
var reqId = ++reportReqId;
|
|
1127
1152
|
props.controller.set({ reportContent: "读取中…" });
|
|
1128
|
-
fetch("/api/dsh-novel-writer/reports?read=" + encodeURIComponent(file), { headers: { accept: "application/json" } }).then(function (r2) { return r2.json(); }).then(function (d) {
|
|
1153
|
+
fetch("/api/dsh-novel-writer/reports?read=" + encodeURIComponent(file), { headers: { accept: "application/json" } }).then(function (r2) { if (!r2.ok) throw new Error("HTTP " + r2.status); return r2.json(); }).then(function (d) {
|
|
1154
|
+
if (reqId !== reportReqId) return; // v3.5.0 #47:快速切换时丢弃过期响应
|
|
1129
1155
|
props.controller.set({ reportContent: d.ok ? JSON.stringify(d.content, null, 2) : ("读取失败:" + (d.error || "")) });
|
|
1130
1156
|
}).catch(function () { props.controller.set({ reportContent: "读取失败(网络/路由不可用)" }); });
|
|
1131
1157
|
};
|
|
@@ -1202,6 +1228,11 @@ var TOOL_GROUPS = [
|
|
|
1202
1228
|
var newBookName = state.creationNewBook || "";
|
|
1203
1229
|
var createNewBook = function () {
|
|
1204
1230
|
var nb = String(newBookName || "").trim();
|
|
1231
|
+
// v3.5.0 #49:客户端校验——路径分隔符/Windows 保留字符直接提示
|
|
1232
|
+
if (nb && (/[\\/:*?"<>|]/.test(nb) || /[.\s]+$/.test(nb) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i.test(nb))) {
|
|
1233
|
+
props.controller.set({ revealMsg: "书名含非法字符(\\ / : * ? \" < > | 或结尾点/空格/系统保留名)", revealErr: true });
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1205
1236
|
if (!nb) {
|
|
1206
1237
|
var elInp = document.getElementById("nwNewBookInput");
|
|
1207
1238
|
if (elInp) { elInp.focus(); elInp.style.borderColor = "#ef4444"; elInp.style.boxShadow = "0 0 0 3px rgba(239,68,68,.15)"; }
|
|
@@ -1220,7 +1251,7 @@ var TOOL_GROUPS = [
|
|
|
1220
1251
|
isDefault ? el("div", { style: { fontSize: "10px", color: "#cbd5e1", marginTop: "1px" } }, t("creation.defaultHint")) : null
|
|
1221
1252
|
),
|
|
1222
1253
|
el("button", { type: "button", onClick: function () { props.controller.set({ creationBook: name, creationDraft: null, creationDirty: false }); props.controller.openView("creation-form"); } }, t("creation.edit")),
|
|
1223
|
-
isDefault ? null : el("button", { type: "button", onClick: function () { var patch = {}; patch[name] = null; props.toggle({ creationProfiles: patch });
|
|
1254
|
+
isDefault ? null : el("button", { type: "button", onClick: function () { var patch = {}; patch[name] = null; props.toggle({ creationProfiles: patch }); props.controller.set({ revealMsg: t("creation.deleted"), revealErr: false }); } }, t("creation.delete"))
|
|
1224
1255
|
);
|
|
1225
1256
|
}
|
|
1226
1257
|
body = el("div", null,
|
|
@@ -1314,6 +1345,8 @@ var TOOL_GROUPS = [
|
|
|
1314
1345
|
var onFeatures = detailFeatures.filter(function (k) { return f[k] !== false; }).length;
|
|
1315
1346
|
var onTools = ALL_TOOLS.filter(function (k) { return tools[k] !== false; }).length;
|
|
1316
1347
|
body = el("div", null,
|
|
1348
|
+
// v3.5.0 #37:保存失败/宿主不可达提示条
|
|
1349
|
+
(state.saveFailed || state.hostOk === false) ? el("div", { key: "saveBanner", style: { padding: "7px 10px", marginBottom: "8px", borderRadius: "8px", background: "#fef3c7", border: "1px solid #fcd34d", color: "#92400e", fontSize: "11px", lineHeight: "1.5" } }, state.hostOk === false ? t("panel.localOnly") : t("panel.saveFailed")) : null,
|
|
1317
1350
|
// v2.6.5:更新提示条(有新版才显示,点击跳 GitHub Release)
|
|
1318
1351
|
state.updateCheck && state.updateCheck.updateAvailable && state.updateCheck.releaseUrl
|
|
1319
1352
|
? el("a", { className: "nwUpdateBar", href: state.updateCheck.releaseUrl, target: "_blank", rel: "noopener noreferrer" },
|
|
@@ -1373,7 +1406,9 @@ var TOOL_GROUPS = [
|
|
|
1373
1406
|
el("div", { key: "statsCard", style: { marginTop: "10px", padding: "10px 12px", borderRadius: "10px", border: "1px solid #e2e8f0", background: "#fff" } },
|
|
1374
1407
|
el("div", { style: { fontSize: "12px", fontWeight: "600", color: "#475569", marginBottom: "6px" } }, "📚 " + t("stats.title")),
|
|
1375
1408
|
el("div", { style: { display: "flex", flexDirection: "column", gap: "5px" } },
|
|
1376
|
-
|
|
1409
|
+
state.booksStats === null
|
|
1410
|
+
? el("div", { style: { fontSize: "11px", color: "#f59e0b" } }, t("stats.pending"))
|
|
1411
|
+
: (state.booksStats || []).length === 0
|
|
1377
1412
|
? el("div", { style: { fontSize: "11px", color: "#94a3b8" } }, t("stats.empty"))
|
|
1378
1413
|
: (state.booksStats || []).slice(0, 6).map(function (bs) {
|
|
1379
1414
|
var charsText = bs.chars >= 10000 ? (bs.chars / 10000).toFixed(1) + "万" : String(bs.chars || 0);
|
|
@@ -1390,7 +1425,7 @@ var TOOL_GROUPS = [
|
|
|
1390
1425
|
// v3.2.0:报告历史入口
|
|
1391
1426
|
el("button", { key: "reportsEntry", type: "button", style: { display: "flex", alignItems: "center", width: "100%", marginTop: "8px", padding: "9px 12px", borderRadius: "10px", border: "1px solid #e2e8f0", background: "#fff", cursor: "pointer", textAlign: "left" }, onClick: function () {
|
|
1392
1427
|
props.controller.set({ reportsGroups: null, reportContent: null, reportLoading: true });
|
|
1393
|
-
fetch("/api/dsh-novel-writer/reports", { headers: { accept: "application/json" } }).then(function (r2) { return r2.json(); }).then(function (d) {
|
|
1428
|
+
fetch("/api/dsh-novel-writer/reports", { headers: { accept: "application/json" } }).then(function (r2) { if (!r2.ok) throw new Error("HTTP " + r2.status); return r2.json(); }).then(function (d) {
|
|
1394
1429
|
props.controller.set({ reportsGroups: d.groups || [], reportLoading: false });
|
|
1395
1430
|
}).catch(function () { props.controller.set({ reportsGroups: [], reportLoading: false }); });
|
|
1396
1431
|
props.controller.openView("reports");
|
|
@@ -1404,7 +1439,7 @@ var TOOL_GROUPS = [
|
|
|
1404
1439
|
el("button", { type: "button", disabled: state.demoLoading, style: { display: "flex", alignItems: "center", gap: "8px", width: "100%", background: "transparent", border: "none", padding: 0, cursor: state.demoLoading ? "default" : "pointer", textAlign: "left" }, onClick: function () {
|
|
1405
1440
|
if (state.demoLoading) return;
|
|
1406
1441
|
props.controller.set({ demoLoading: true });
|
|
1407
|
-
fetch("/api/dsh-novel-writer/demo", { headers: { accept: "application/json" } }).then(function (r2) { return r2.json(); }).then(function (d) {
|
|
1442
|
+
fetch("/api/dsh-novel-writer/demo", { headers: { accept: "application/json" } }).then(function (r2) { if (!r2.ok) throw new Error("HTTP " + r2.status); return r2.json(); }).then(function (d) {
|
|
1408
1443
|
props.controller.set({ demoReport: d.ok ? d.report : ("演示失败:" + (d.error || "")), demoLoading: false });
|
|
1409
1444
|
}).catch(function () { props.controller.set({ demoReport: "演示失败(网络/路由不可用)", demoLoading: false }); });
|
|
1410
1445
|
} },
|
|
@@ -1606,6 +1641,7 @@ var TOOL_GROUPS = [
|
|
|
1606
1641
|
creationProfile: remote.creationProfile || null,
|
|
1607
1642
|
creationProfiles: remote.creationProfiles || {},
|
|
1608
1643
|
books: remote.books || [],
|
|
1644
|
+
booksStats: typeof remote.booksStats === "undefined" ? null : (remote.booksStats || []),
|
|
1609
1645
|
dirs: remote.dirs || null,
|
|
1610
1646
|
file: remote.file || "",
|
|
1611
1647
|
source: "host",
|
|
@@ -1619,6 +1655,12 @@ var TOOL_GROUPS = [
|
|
|
1619
1655
|
controller.set({
|
|
1620
1656
|
enabled: typeof local.enabled === "boolean" ? local.enabled : true,
|
|
1621
1657
|
autoAnalyze: typeof local.autoAnalyze === "boolean" ? local.autoAnalyze : true,
|
|
1658
|
+
// v3.5.0 #38:完整恢复本地降级状态
|
|
1659
|
+
tools: local.tools || {},
|
|
1660
|
+
features: local.features || {},
|
|
1661
|
+
styleTolerance: local.styleTolerance || null,
|
|
1662
|
+
creationProfile: local.creationProfile || null,
|
|
1663
|
+
creationProfiles: local.creationProfiles || {},
|
|
1622
1664
|
source: "local",
|
|
1623
1665
|
hostOk: false,
|
|
1624
1666
|
loading: false
|
|
@@ -1663,7 +1705,8 @@ var TOOL_GROUPS = [
|
|
|
1663
1705
|
controller.set({ hostOk: false, saveFailed: true, source: "local" });
|
|
1664
1706
|
try {
|
|
1665
1707
|
var snapshot = controller.getSnapshot();
|
|
1666
|
-
|
|
1708
|
+
// v3.5.0 #38:宿主不可达时完整持久化(刷新不丢 tools/features/容差/设定)
|
|
1709
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify({ enabled: snapshot.enabled, autoAnalyze: snapshot.autoAnalyze, tools: snapshot.tools || {}, features: snapshot.features || {}, styleTolerance: snapshot.styleTolerance || null, creationProfile: snapshot.creationProfile || null, creationProfiles: snapshot.creationProfiles || {} }));
|
|
1667
1710
|
} catch (error) { /* ignore */ }
|
|
1668
1711
|
};
|
|
1669
1712
|
controller.setToggle(toggle);
|
package/lib/embedding.js
CHANGED
|
@@ -23,6 +23,8 @@ try {
|
|
|
23
23
|
}
|
|
24
24
|
} catch { /* 旧 Node:回退纯 JSON 缓存 */ }
|
|
25
25
|
const MODEL_DIR = path.join(__dirname, "models");
|
|
26
|
+
const MODEL_NAME = "bge-small-zh-v1.5"; // v3.5.0 #55:索引缓存模型标识(换模型旧缓存作废)
|
|
27
|
+
const EMBED_DIM = 128; // v3.5.0 #55:缓存向量维度(降采样后)
|
|
26
28
|
const EMBEDDING_CACHE_ROOT = ".novel-writer";
|
|
27
29
|
|
|
28
30
|
let enginePromise = null;
|
|
@@ -113,8 +115,11 @@ async function embedMany(chunks) {
|
|
|
113
115
|
const worker = async () => {
|
|
114
116
|
while (cursor < chunks.length) {
|
|
115
117
|
const i = cursor; cursor += 1;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
// v3.5.0 #53:单块容错——一块 embed 抛错(WASM 抖动/超长)不拖垮整本书索引
|
|
119
|
+
try {
|
|
120
|
+
const vec = await embed(chunks[i].text);
|
|
121
|
+
out[i] = vec ? { id: chunks[i].id, text: chunks[i].text, vec } : null;
|
|
122
|
+
} catch { out[i] = null; }
|
|
118
123
|
}
|
|
119
124
|
};
|
|
120
125
|
await Promise.all(Array.from({ length: 4 }, worker));
|
|
@@ -279,6 +284,8 @@ function loadIndex(root, book) {
|
|
|
279
284
|
try {
|
|
280
285
|
const data = readIndexData(root, book);
|
|
281
286
|
if (!data) return null;
|
|
287
|
+
// v3.5.0 #55:与 loadIndexMeta 一致——model/dim 不符视为无缓存
|
|
288
|
+
if (data.model !== MODEL_NAME || data.dim !== EMBED_DIM) return null;
|
|
282
289
|
return data.items.map((item) => ({ id: item.id, text: item.t, vec: item.v, chapter: item.ch ?? null }));
|
|
283
290
|
} catch { return null; }
|
|
284
291
|
}
|
|
@@ -288,6 +295,8 @@ function loadIndexMeta(root, book) {
|
|
|
288
295
|
try {
|
|
289
296
|
const data = readIndexData(root, book);
|
|
290
297
|
if (!data) return { items: null, fp: null };
|
|
298
|
+
// v3.5.0 #55:缓存校验 model/dim——换模型/维度变化时旧缓存作废(触发重建),防 cosine 恒 0
|
|
299
|
+
if (data.model !== MODEL_NAME || data.dim !== EMBED_DIM) return { items: null, fp: null };
|
|
291
300
|
return {
|
|
292
301
|
items: data.items.map((item) => ({ id: item.id, text: item.t, th: item.th ?? null, vec: item.v, chapter: item.ch ?? null })),
|
|
293
302
|
fp: data.fp ?? null
|