dsh-novel-writer 2.0.2 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -8,7 +8,7 @@ English | [**中文**](./README.md)
8
8
  [![GitHub release](https://img.shields.io/github/v/release/siweina/dsh-novel-writer.svg?style=flat-square)](https://github.com/siweina/dsh-novel-writer/releases)
9
9
  [![DSH plugin](https://img.shields.io/badge/DSH-plugin-4b8bbe.svg?style=flat-square)](https://github.com/deepseek-ai/deepseek-harness)
10
10
 
11
- A novel-writing assistant plugin for **DeepSeek Harness (DSH)**: chapter library management, sentence-pattern analysis, emotion purification & quantification, style checking, plot tracking, settings management, worldview/pragmatics detection, **local semantic search (0 token)**, batch import, and AI-assisted continuation writing. Zero third-party dependencies on host.
11
+ A novel-writing assistant plugin for **DeepSeek Harness (DSH)** (v2.5.0): chapter library management, sentence-pattern analysis, emotion purification & quantification, **12-axis vibe spectrum**, **style portrait report**, plot & settings management, local semantic search (0 token), webnovel signal detection, batch import, and AI-assisted continuation writing. Zero third-party dependencies on host.
12
12
 
13
13
  ---
14
14
 
@@ -28,40 +28,24 @@ npm install dsh-novel-writer
28
28
  dsh plugin --profile web add github:siweina/dsh-novel-writer#main
29
29
  ```
30
30
 
31
- After installing, **restart the web app** to activate (host registers 14 tools + state/reveal routes; browser mounts the "Writing Assistant" sidebar toggle panel).
32
-
33
- ---
34
-
35
- ## v2.0.0 New: Local Semantic Engine (0 token)
36
-
37
- - 🧠 **Built-in Chinese semantic model** (bge-small-zh-v1.5 quantized, 23MB, shipped with the plugin): 512-dim vectors, local CPU inference, **zero API cost**;
38
- - 🔍 **novel_semantic_search**: search the whole book with natural language for semantically related passages (foreshadowing/emotional scenes/settings), even without matching keywords;
39
- - 📊 **Semantic style comparison**: novel_style_check adds a semantic-similarity dimension beyond rule-based fingerprints;
40
- - 💭 **Semantic implicit emotion**: emotion prototype sentences scan the book index to find "imagery passages outside the word list";
41
- - 🪶 Lazy loading + automatic fallback to pure-rule mode (never breaks existing features).
31
+ After installing, **restart the web app** to activate (host registers 15 tools + state/reveal routes; browser mounts the "Writing Assistant" sidebar toggle panel).
42
32
 
43
33
  ---
44
34
 
45
35
  ## Features
46
36
 
47
- 1. **Chapter library**: chapters in `novels/<book>/第N章.md` (or .txt/.markdown); auto encoding detection (UTF-8/UTF-16/GBK).
48
- 2. **Sentence-pattern analysis**: 9 categories, arrangement patterns, rhythm, emotion curve, style fingerprint + guidance, with cache & report export.
49
- 3. **Emotion purification**: strong/weak emotion-word grading, pollution-source detection, caveat warning + AI re-verification, cleanDominant true baseline.
50
- 4. **Emotion quantification** (v1.6.0): Valence mapping + sliding window variance V / delta Δ / conflict index C; implicit imagery carriers + explicit-implicit conflict; complexity score + composite emotion pairs.
51
- 5. **Style check**: chapter vs book similarity + deviation list + advice.
52
- 6. **Plot tracking**: foreshadowing/plot-hook registry (open/done), typed fields + auto mention tracking.
53
- 7. **Settings management**: five tables characters / locations / items / timeline / worldview.
54
- 8. **Worldview & pragmatics detection**: auto cultural-baseline detection with confidence; speechStyle title/honorifics/rituals/tone norms.
55
- 9. **Genre & theme detection**: primary/secondary themes + genre, low-frequency noise filtered.
56
- 10. **Chapter summaries**: per-chapter digest + key events for long-book continuation.
57
- 11. **Continuity audit**: settings conflicts + pragmatics conflicts with replacement suggestions.
58
- 12. **Batch import**: auto-detect book names & chapter numbers, classified import.
59
- 13. **Continuation writing**: read-first workflow; keeps style/foreshadowing/worldview consistent.
60
- 14. **Per-tool UI toggles**: "Writing Assistant" sidebar panel (master + per-tool + feature toggles).
37
+ 1. **Style portrait report** (`novel_style_report`, v2.5.0): 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.
38
+ 2. **12-axis vibe spectrum** (v2.5.0): nightmare / angst / heartwarming / fluff / tearjerker / dark / mystery / blaze / absurd / lonesome / aesthetic / sensual — with traceable evidence, 0 token.
39
+ 3. **Local semantic engine** (v2.0.0): bge-small-zh Chinese model (23MB, shipped with the plugin) local CPU inference — `novel_semantic_search` finds semantically related passages with natural language, semantic style comparison, semantic implicit emotion; lazy loading + graceful fallback.
40
+ 4. **Sentence-pattern analysis**: 9 categories, arrangement patterns, rhythm, emotion curve, style fingerprint + guidance, with cache & report export.
41
+ 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.
42
+ 6. **Worldview & pragmatics detection**: auto cultural-baseline detection with confidence; speechStyle title/honorifics/rituals/tone norms; genre & theme + webnovel signals (v2.2).
43
+ 7. **Writing toolkit**: plot tracking / five settings tables (characters·locations·items·timeline·worldview) / chapter summaries / continuity audit / batch import / style check / continuation writing.
44
+ 8. **Per-tool UI toggles**: "Writing Assistant" sidebar panel (master + grouped tool toggles + feature toggles), plain-language labels.
61
45
 
62
46
  ---
63
47
 
64
- ## Provided Tools (14)
48
+ ## Provided Tools (15)
65
49
 
66
50
  | Tool | Description |
67
51
  |------|-------------|
@@ -74,11 +58,12 @@ After installing, **restart the web app** to activate (host registers 14 tools +
74
58
  | `novel_sentence_analysis` | Sentence-pattern analysis |
75
59
  | `novel_sentence_config` | View/set tool & feature toggles |
76
60
  | `novel_style_check` | Style check (rule + semantic) |
61
+ | `novel_style_report` | **Style portrait report** (6-dim measurement) |
77
62
  | `novel_plot` | Plot/foreshadowing tracker |
78
63
  | `novel_settings` | Settings management (+worldview) |
79
64
  | `novel_summary` | Chapter summaries |
80
65
  | `novel_continuity_check` | Continuity audit |
81
- | `novel_semantic_search` | **Semantic search** (v2.0.0, local embedding, 0 token) |
66
+ | `novel_semantic_search` | Semantic search (local embedding, 0 token) |
82
67
 
83
68
  ---
84
69
 
@@ -95,7 +80,7 @@ After installing, **restart the web app** to activate (host registers 14 tools +
95
80
 
96
81
  ## Data Directory
97
82
 
98
- Under `<library-root>/.novel-writer/`: `plots` / `settings` / `summaries` / `analysis` / `audits` / `embedding` (semantic index cache).
83
+ Under `<library-root>/.novel-writer/`: `plots` / `settings` / `summaries` / `analysis` / `audits` / `embedding` / `style-reports`.
99
84
 
100
85
  ---
101
86
 
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  [![GitHub release](https://img.shields.io/github/v/release/siweina/dsh-novel-writer.svg?style=flat-square)](https://github.com/siweina/dsh-novel-writer/releases)
9
9
  [![DSH plugin](https://img.shields.io/badge/DSH-plugin-4b8bbe.svg?style=flat-square)](https://github.com/deepseek-ai/deepseek-harness)
10
10
 
11
- 为 **DeepSeek Harness (DSH)** 打造的小说写作助手插件:章节库管理、句式模式分析、情感净化与量化、风格自检、伏笔登记、设定管理、世界观/语用检测、**本地语义检索(0 token)**、批量导入与 AI 续写辅助。宿主端零第三方依赖,浏览器端仅依赖 Web GUI 自带的 react。
11
+ 为 **DeepSeek Harness (DSH)** 打造的小说写作助手插件(v2.5.0):章节库管理、句式分析、情感净化与量化、氛围光谱、**风格画像报告**、伏笔设定管理、本地语义检索(0 token)、网文信号识别、批量导入与 AI 续写辅助。宿主端零第三方依赖,浏览器端仅依赖 Web GUI 自带的 react。
12
12
 
13
13
  ---
14
14
 
@@ -28,40 +28,24 @@ npm install dsh-novel-writer
28
28
  dsh plugin --profile web add github:siweina/dsh-novel-writer#main
29
29
  ```
30
30
 
31
- 安装后**重启 web 应用**生效(宿主端注册 14 个工具与 state/reveal 路由,浏览器端挂载侧边栏「写作助手功能」开关面板)。
32
-
33
- ---
34
-
35
- ## v2.0.0 新增:本地语义引擎(0 token)
36
-
37
- - 🧠 **内置中文语义模型**(bge-small-zh-v1.5 quantized,23MB,随插件分发):512 维向量,本地 CPU 推理,**零 API 费用**;
38
- - 🔍 **novel_semantic_search**:用自然语言搜全书语义相关段落(伏笔/情感场景/设定提及),即使原文没有相同关键词也能命中;
39
- - 📊 **语义级风格对比**:novel_style_check 在规则指纹之外新增语义相似度维度;
40
- - 💭 **语义隐性情感**:情感原型句扫全书索引,发现"词表外疑似意象段落";
41
- - 🪶 懒加载 + 失败自动回退纯规则(不影响任何既有功能)。
31
+ 安装后**重启 web 应用**生效(宿主端注册 15 个工具与 state/reveal 路由,浏览器端挂载侧边栏「写作助手功能」开关面板)。
42
32
 
43
33
  ---
44
34
 
45
35
  ## 功能
46
36
 
47
- 1. **章节库管理**:章节存放于 `novels/<书名>/第N章.md`(或 .txt/.markdown),编码自动探测(UTF-8/UTF-16/GBK)。
48
- 2. **句式模式分析**:九类句式分布、排列规律、句长节奏、情感曲线、风格指纹与节奏建议,带缓存与报告导出。
49
- 3. **情感净化**:强/弱情绪词分级、污染源检测、caveat 预警 + AI 复核闭环、cleanDominant 真实基调。
50
- 4. **情感量化**(v1.6.0):Valence 效价映射 + 滑动窗口 → 方差 V / 斜率 Δ / 矛盾指数 C;隐性意象载体 + 显隐冲突;复杂度评分 + 复合情感共现。
51
- 5. **风格自检**:章节 vs 全书相似度 + 偏差清单 + 续写建议。
52
- 6. **伏笔登记表**:open/done 状态、字段化登记、章节提及自动追踪。
53
- 7. **设定管理**:人物/地点/道具/时间线/世界观 五张表。
54
- 8. **世界观与语用检测**:自动判断文化基准(西/东/混合)+ 置信度;speechStyle 称谓/客套/仪式/语气规范。
55
- 9. **题材与流派检测**:主副题材 + 流派识别,低频噪音自动过滤。
56
- 10. **章节摘要**:每章摘要 + 关键事件,长书续写先读摘要。
57
- 11. **连贯性审计**:设定矛盾 + 语用冲突候选,带建议替换。
58
- 12. **批量导入**:原稿件自动识别书名与章节号分类导入。
59
- 13. **续写辅助**:先读后写、保持文风/伏笔/世界观一致。
60
- 14. **全工具 UI 开关**:侧边栏「写作助手功能」面板(总开关 + 每工具开关 + 功能开关)。
37
+ 1. **风格画像报告**(novel_style_report,v2.5.0):6 维测量报告——文风指纹 / 高频词汇 / 题材流派 / 情感量化 / 氛围光谱 12 轴 / 语义风格距离。**测量与判断分离**:插件只报数不贴标签,AI 判断可回传存盘(`.novel-writer/style-reports/`),续写保持风格一致。
38
+ 2. **氛围光谱 12 轴**(v2.5.0):噩梦感 / 焦虑压抑 / 温馨治愈 / 甜宠日常 / 催泪虐心 / 黑暗残酷 / 悬疑神秘 / 热血激昂 / 荒诞无厘头 / 孤独疏离 / 文艺唯美 / 情欲暧昧——证据链可追溯,0 token。
39
+ 3. **本地语义引擎**(v2.0.0):bge-small-zh 中文模型(23MB 随插件分发)本地 CPU 推理——`novel_semantic_search` 自然语言搜全书语义相关段落,语义级风格对比、语义隐性情感,懒加载 + 自动回退。
40
+ 4. **句式模式分析**:九类句式分布、排列规律、句长节奏、情感曲线、风格指纹与节奏建议,带缓存与报告导出。
41
+ 5. **情感净化 + 量化**:强/弱情绪词分级、污染源检测、caveat 预警 + AI 复核;Valence 滑动窗口 方差 V / 斜率 Δ / 矛盾指数 C + 隐性意象载体。
42
+ 6. **世界观与语用检测**:文化基准自动判断(西/东/混合)+ 置信度;speechStyle 称谓/客套/仪式/语气规范;题材流派 + 网文信号(v2.2)。
43
+ 7. **写作辅助全家桶**:伏笔登记表 / 设定五张表(人物·地点·道具·时间线·世界观)/ 章节摘要 / 连贯性审计 / 批量导入 / 风格自检 / 续写辅助。
44
+ 8. **全工具 UI 开关**:侧边栏「写作助手功能」面板(总开关 + 工具开关分组 + 功能开关),大白话文案。
61
45
 
62
46
  ---
63
47
 
64
- ## 提供的工具(14 个)
48
+ ## 提供的工具(15 个)
65
49
 
66
50
  | 工具 | 说明 |
67
51
  |------|------|
@@ -74,11 +58,12 @@ dsh plugin --profile web add github:siweina/dsh-novel-writer#main
74
58
  | `novel_sentence_analysis` | 句式模式分析(九类/情感净化/量化/曲线/指纹) |
75
59
  | `novel_sentence_config` | 查看/修改工具与功能开关 |
76
60
  | `novel_style_check` | 风格自检(规则+语义双维度) |
61
+ | `novel_style_report` | **风格画像报告**(6 维测量 + AI 判断分离) |
77
62
  | `novel_plot` | 伏笔/剧情线登记表 |
78
63
  | `novel_settings` | 设定管理(人物/地点/道具/时间线/世界观) |
79
64
  | `novel_summary` | 章节摘要(长书续写辅助) |
80
65
  | `novel_continuity_check` | 连贯性审计(设定矛盾+语用冲突候选) |
81
- | `novel_semantic_search` | **语义检索**(v2.0.0,本地 embedding,0 token) |
66
+ | `novel_semantic_search` | 语义检索(本地 embedding,0 token) |
82
67
 
83
68
  ---
84
69
 
@@ -95,7 +80,7 @@ dsh plugin --profile web add github:siweina/dsh-novel-writer#main
95
80
 
96
81
  ## 数据目录
97
82
 
98
- `<书库根>/.novel-writer/`:`plots`(伏笔)/ `settings`(设定)/ `summaries`(摘要)/ `analysis`(分析报告)/ `audits`(审计)/ `embedding`(语义索引缓存)。
83
+ `<书库根>/.novel-writer/`:`plots`(伏笔)/ `settings`(设定)/ `summaries`(摘要)/ `analysis`(分析报告)/ `audits`(审计)/ `embedding`(语义索引)/ `style-reports`(风格画像)。
99
84
 
100
85
  ---
101
86
 
package/lib/analysis.js CHANGED
@@ -347,6 +347,28 @@ function emotionOf(text) {
347
347
  cleanDominant = emotion;
348
348
  }
349
349
  }
350
+ // v2.5 修复:dutir_seven.json(大连理工 27,413 词)兜底——小词表未覆盖的情感词也计分
351
+ // 性能安全:只用"文本中出现的相邻二字组"查表(O(句长)),不做 27k 词全表扫描
352
+ try {
353
+ const seenWords = new Set();
354
+ for (const list of Object.values(words)) for (const w of list) seenWords.add(w);
355
+ dutirEmotion(""); // 确保 dutirLookup 已构建(懒加载)
356
+ const bigrams = text.match(/[\u4e00-\u9fa5]{2}/g) || [];
357
+ for (const w of new Set(bigrams)) {
358
+ if (seenWords.has(w)) continue;
359
+ const emo = dutirLookup.get(w);
360
+ if (!emo) continue;
361
+ scores[emo] += 1;
362
+ cleanScores[emo] += 1;
363
+ words[emo].push(w);
364
+ seenWords.add(w);
365
+ }
366
+ dominant = "neutral";
367
+ let best = 0;
368
+ for (const emotion of ["joy", "anger", "sorrow", "fear", "surprise"]) {
369
+ if (scores[emotion] > best) { best = scores[emotion]; dominant = emotion; }
370
+ }
371
+ } catch { /* dutir 兜底失败不影响原结果 */ }
350
372
  return {
351
373
  scores,
352
374
  words,
@@ -474,18 +496,143 @@ const IMPLICIT_CARRIERS = Object.freeze([
474
496
  { words: ["垂手", "放下手", "手垂落"], valence: -0.15, label: "无力·放弃" }
475
497
  ]);
476
498
 
499
+ /**
500
+ * v2.1.0 多方向意象表:同一载体在不同语境可表达不同情感("变色龙词")。
501
+ * 每个词允许多个方向条目,各自带触发语境词(规则层裁决):
502
+ * - 命中触发词 → 按该方向计分
503
+ * - 无触发词 → 交给语义层(与正/负原型句比相似度)
504
+ * - 语义层也不确定 → 双计分 + 歧义标记
505
+ */
506
+ const AMBIGUOUS_CARRIERS = Object.freeze({
507
+ "雨": [
508
+ { valence: -0.3, label: "压抑·萧瑟", triggers: ["冷", "夜", "秋", "寒", "阴", "灰", "敲", "不停", "绵", "细", "孤"] },
509
+ { valence: 0.2, label: "清新·复苏", triggers: ["晴", "彩虹", "洗净", "春", "润", "后", "停", "甘"] }
510
+ ],
511
+ "烛火": [
512
+ { valence: -0.2, label: "诡异·不安", triggers: ["摇曳", "昏", "暗", "影", "鬼", "摇", "颤", "燃尽", "跳"] },
513
+ { valence: 0.25, label: "温馨", triggers: ["暖", "炉", "家", "围", "饭", "柔", "亮"] }
514
+ ],
515
+ "火光": [
516
+ { valence: -0.25, label: "灾难·恐惧", triggers: ["烧", "浓烟", "废墟", "惨叫", "逃", "夜", "红"] },
517
+ { valence: 0.2, label: "希望·温暖", triggers: ["暖", "黎明", "亮", "驱散", "炉", "温"] }
518
+ ],
519
+ "夜": [
520
+ { valence: -0.2, label: "孤独·恐惧", triggers: ["深", "黑", "静", "冷", "无眠", "怕", "漫长", "沉"] },
521
+ { valence: 0.15, label: "安宁·静谧", triggers: ["星", "月", "静好", "温柔", "安"] }
522
+ ],
523
+ "风": [
524
+ { valence: -0.15, label: "萧瑟·离别", triggers: ["冷", "寒", "秋", "吹散", "凛冽", "呜咽"] },
525
+ { valence: 0.15, label: "清爽·自由", triggers: ["暖", "春", "清新", "拂", "轻", "晴"] }
526
+ ],
527
+ "海": [
528
+ { valence: -0.3, label: "深邃·恐惧", triggers: ["黑", "沉", "浪", "吞", "潮", "深", "暗", "涌"] },
529
+ { valence: 0.2, label: "开阔·浪漫", triggers: ["蓝", "晴", "暖", "浪花", "笑", "沙滩", "夕阳"] }
530
+ ],
531
+ "灯": [
532
+ { valence: -0.15, label: "孤独·守望", triggers: ["孤", "昏", "暗", "残", "灭", "夜"] },
533
+ { valence: 0.2, label: "温暖·归处", triggers: ["暖", "亮", "家", "等", "柔", "光"] }
534
+ ],
535
+ "影子": [
536
+ { valence: -0.25, label: "不安·诡异", triggers: ["长", "暗", "摇晃", "鬼", "拖", "黑"] },
537
+ { valence: 0.1, label: "陪伴", triggers: ["暖", "短", "依偎"] }
538
+ ],
539
+ "笑": [
540
+ { valence: -0.15, label: "苦笑·强颜", triggers: ["苦", "勉", "僵", "假", "惨", "涩", "硬"] },
541
+ { valence: 0.25, label: "欢乐", triggers: ["开怀", "灿烂", "暖", "甜", "大", "爽朗", "咯咯"] }
542
+ ],
543
+ "眼泪": [
544
+ { valence: -0.3, label: "悲伤", triggers: ["落", "流", "止不住", "擦", "咸", "含", "忍"] },
545
+ { valence: 0.15, label: "感动·释然", triggers: ["感动", "幸福", "喜极", "温暖", "笑"] }
546
+ ],
547
+ "沉默": [
548
+ { valence: -0.2, label: "压抑·隔阂", triggers: ["久", "冷", "尴尬", "低头", "死寂", "不开口"] },
549
+ { valence: 0.1, label: "默契·安宁", triggers: ["温柔", "懂", "默契", "安静", "并肩"] }
550
+ ],
551
+ "花开": [
552
+ { valence: -0.1, label: "易逝·伤春", triggers: ["落", "谢", "春尽", "残"] },
553
+ { valence: 0.25, label: "美好·希望", triggers: ["盛", "香", "春", "灿烂", "暖"] }
554
+ ],
555
+ "黄昏": [
556
+ { valence: -0.25, label: "迟暮·萧瑟", triggers: ["残", "落", "暗", "孤", "冷", "尽"] },
557
+ { valence: 0.15, label: "温柔·归家", triggers: ["暖", "金", "柔", "炊烟", "并肩"] }
558
+ ],
559
+ "奔跑": [
560
+ { valence: -0.2, label: "逃离·慌乱", triggers: ["逃", "拼命", "慌", "追", "喘", "夜"] },
561
+ { valence: 0.2, label: "自由·奔赴", triggers: ["向", "奔", "扑", "迎", "笑", "阳光"] }
562
+ ]
563
+ });
564
+
565
+ /** v2.1.0:查找多方向载体命中(返回所有方向条目 + 上下文命中触发词)。 */
566
+ export function matchAmbiguousCarriers(text) {
567
+ const hits = [];
568
+ for (const [word, entries] of Object.entries(AMBIGUOUS_CARRIERS)) {
569
+ let idx = 0;
570
+ while ((idx = text.indexOf(word, idx)) !== -1) {
571
+ // 上下文窗口:前后 30 字
572
+ const ctx = text.slice(Math.max(0, idx - 30), idx + word.length + 30);
573
+ const matched = entries.map((e) => {
574
+ const trigger = e.triggers.find((t) => ctx.includes(t));
575
+ return { ...e, triggerHit: trigger || null };
576
+ });
577
+ hits.push({ word, ctx, entries: matched });
578
+ idx += word.length;
579
+ }
580
+ }
581
+ return hits;
582
+ }
583
+
477
584
  /** v1.6.0 隐性载体扫描:意象/动作 → 负向/正向占比 + top 载体 + 脆弱标记。 */
478
- export function implicitEmotionScan(text) {
585
+ export function implicitEmotionScan(text, semResolver = null) {
479
586
  let negHits = 0, posHits = 0, fragileHits = 0;
587
+ let ambHits = 0;
480
588
  const carrierCounts = new Map();
589
+ const ambiguous = [];
590
+ const see = (label, word, n, valence) => {
591
+ if (valence < 0) negHits += n * -valence;
592
+ else { posHits += n * valence; if (false) fragileHits += n; }
593
+ carrierCounts.set(label + ":" + word, (carrierCounts.get(label + ":" + word) ?? 0) + n);
594
+ };
595
+ // ① 单方向表(原有)
481
596
  for (const carrier of IMPLICIT_CARRIERS) {
482
597
  for (const word of carrier.words) {
483
598
  const n = text.split(word).length - 1;
484
599
  if (n > 0) {
485
- if (carrier.valence < 0) negHits += n * -carrier.valence;
486
- else { posHits += n * carrier.valence; if (carrier.fragile) fragileHits += n; }
487
- carrierCounts.set(carrier.label + ":" + word, (carrierCounts.get(carrier.label + ":" + word) ?? 0) + n);
600
+ see(carrier.label, word, n, carrier.valence);
601
+ if (carrier.valence >= 0 && carrier.fragile) fragileHits += n;
602
+ }
603
+ }
604
+ }
605
+ // ② 多方向表(变色龙词):规则触发 → 定方向;无触发 → 语义层;都不确定 → 双计分+歧义
606
+ for (const [word, entries] of Object.entries(AMBIGUOUS_CARRIERS)) {
607
+ let idx = 0;
608
+ while ((idx = text.indexOf(word, idx)) !== -1) {
609
+ const ctx = text.slice(Math.max(0, idx - 30), idx + word.length + 30);
610
+ const triggerMatched = entries.filter((e) => e.triggers.some((t) => ctx.includes(t)));
611
+ if (triggerMatched.length === 1) {
612
+ const e = triggerMatched[0];
613
+ see(e.label, word, 1, e.valence);
614
+ } else if (triggerMatched.length > 1) {
615
+ // 多个触发同时命中:双计分 + 歧义
616
+ for (const e of triggerMatched) see(e.label, word, 1, e.valence * 0.5);
617
+ ambHits += 1;
618
+ ambiguous.push({ word, reason: "多触发冲突", ctx: ctx.trim().slice(0, 40) });
619
+ } else if (semResolver) {
620
+ // 无触发 → 语义层裁决(异步由调用方包装)
621
+ const r = semResolver(ctx, entries);
622
+ if (r && r.resolved) see(r.label, word, 1, r.valence);
623
+ else {
624
+ // 语义不确定 → 双计分 + 歧义标记
625
+ for (const e of entries) see(e.label, word, 1, e.valence * 0.5);
626
+ ambHits += 1;
627
+ ambiguous.push({ word, reason: "语义不确定", ctx: ctx.trim().slice(0, 40) });
628
+ }
629
+ } else {
630
+ // 无语义层可用 → 按词表首条目弱计分 + 歧义
631
+ for (const e of entries.slice(0, 2)) see(e.label, word, 1, e.valence * 0.5);
632
+ ambHits += 1;
633
+ ambiguous.push({ word, reason: "未裁决", ctx: ctx.trim().slice(0, 40) });
488
634
  }
635
+ idx += word.length;
489
636
  }
490
637
  }
491
638
  const total = negHits + posHits;
@@ -493,8 +640,10 @@ export function implicitEmotionScan(text) {
493
640
  return {
494
641
  negative: total === 0 ? 0 : Math.round((negHits / total) * 100) / 100,
495
642
  positive: total === 0 ? 0 : Math.round((posHits / total) * 100) / 100,
643
+ ambiguousRatio: total === 0 ? 0 : Math.round((ambHits / (total + ambHits)) * 100) / 100,
496
644
  fragile: fragileHits > 0,
497
645
  totalHits: negHits + posHits,
646
+ ambiguous,
498
647
  topCarriers: top
499
648
  };
500
649
  }
@@ -675,9 +824,9 @@ export function emotionComplexity(perChapter) {
675
824
  /**
676
825
  * v1.6.0 情感量化入口:三指标 + 显隐对比 + 复杂度 + 复合共现(纯规则 0 token)。
677
826
  */
678
- export function emotionalQuantification(text, perChapter, blocks) {
827
+ export function emotionalQuantification(text, perChapter, blocks, semResolver = null) {
679
828
  const stats = valenceStats(text);
680
- const implicit = implicitEmotionScan(text);
829
+ const implicit = implicitEmotionScan(text, semResolver);
681
830
  const meanValence = stats.meanValence ?? 0;
682
831
  // perChapter 为空(单章/无分章输入)时,用当前文本自身 clean 计数聚合,保证 complexity 恒有值
683
832
  if (!Array.isArray(perChapter) || perChapter.length === 0) {
@@ -942,7 +1091,7 @@ export function analyzeText(text, options = {}) {
942
1091
  .slice(0, 12),
943
1092
  curve: emotionCurve(blockMeta, curveSegments),
944
1093
  // v1.6.0:情感量化(Valence 三指标 + 显隐对比 + 复杂度 + 复合共现)
945
- quantification: emotionalQuantification(text, [], blockMeta.map((m) => m.sentences).filter((s) => s.length > 0))
1094
+ quantification: emotionalQuantification(text, [], blockMeta.map((m) => m.sentences).filter((s) => s.length > 0), options.semResolver || null)
946
1095
  };
947
1096
 
948
1097
  // 主观性指数(启发式 0-100)
@@ -1150,3 +1299,34 @@ export function densityOf(text) {
1150
1299
  sense: { ...sense }
1151
1300
  };
1152
1301
  }
1302
+
1303
+
1304
+ // v2.5 修复:接入 lib/lexicons/dutir_seven.json(大连理工七类情感词表 27,413 词)
1305
+ // 懒加载 + 内存缓存;七类 → 五情感映射,供 sentimentCounts 扩展计数(词表未覆盖的词也能计分)
1306
+ import { createRequire } from "node:module";
1307
+ const __req = createRequire(import.meta.url);
1308
+ let DUTIR = null;
1309
+ function loadDutir() {
1310
+ if (DUTIR) return DUTIR;
1311
+ try {
1312
+ const p = __req.resolve("./lexicons/dutir_seven.json");
1313
+ DUTIR = JSON.parse(__req("node:fs").readFileSync(p, "utf8"));
1314
+ } catch {
1315
+ DUTIR = {};
1316
+ }
1317
+ return DUTIR;
1318
+ }
1319
+ const DUTIR_TO_EMOTION = { 乐: "joy", 好: "joy", 怒: "anger", 哀: "sorrow", 惧: "fear", 恶: "sorrow", 惊: "surprise" };
1320
+ const dutirLookup = new Map();
1321
+ function dutirEmotion(word) {
1322
+ if (dutirLookup.size === 0) {
1323
+ const dutir = loadDutir();
1324
+ for (const [cat, words] of Object.entries(dutir)) {
1325
+ const emo = DUTIR_TO_EMOTION[cat];
1326
+ if (!emo) continue;
1327
+ for (const w of words) if (typeof w === "string" && w.length >= 2) dutirLookup.set(w, emo);
1328
+ }
1329
+ }
1330
+ return dutirLookup.get(word);
1331
+ }
1332
+ export function dutirEmotionOf(word) { return dutirEmotion(word); }