dsh-novel-writer 3.5.0 → 3.7.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/cordis.patch.yml CHANGED
@@ -1,4 +1,4 @@
1
- # dsh-novel-writer v3.5.0 bundle patch: 16 tools (incl. novel_semantic_search) + local embedding engine.
1
+ # dsh-novel-writer v3.7.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
@@ -55,18 +55,19 @@ const STRONG_EMOTION_WORDS = Object.freeze({
55
55
  surprise: ["震惊", "目瞪口呆", "难以置信", "不可思议", "惊愕", "骇然", "震撼"]
56
56
  });
57
57
  const WEAK_EMOTION_WORDS = Object.freeze({
58
- joy: ["兴奋", "满足", "愉快", "痛快", "爽快", "爽", "笑眯眯", "笑容", "微笑", "笑", "哈哈", "乐", "欢", "喜", "高兴", "开心", "喜悦"],
59
- anger: ["生气", "恼火", "恼", "怒", "恨", "气愤", "气", "咬牙", "不满", "发火", "气冲冲"],
60
- sorrow: ["难过", "伤心", "痛苦", "哭泣", "哭", "泪", "眼泪", "流泪", "哽咽", "抽泣", "叹息", "叹气", "失落", "悲"],
58
+ joy: ["兴奋", "满足", "愉快", "痛快", "爽快", "笑眯眯", "笑容", "微笑", "哈哈", "高兴", "开心", "喜悦"],
59
+ anger: ["生气", "恼火", "气愤", "咬牙", "不满", "发火", "气冲冲"],
60
+ sorrow: ["难过", "伤心", "痛苦", "哭泣", "眼泪", "流泪", "哽咽", "抽泣", "叹息", "叹气", "失落", ],
61
61
  fear: ["害怕", "惊慌", "不安", "紧张", "担心", "发抖", "哆嗦", "心慌", "忐忑", "心悸", "心虚", "惊惶"],
62
62
  surprise: ["惊讶", "意外", "吃惊", "诧异", "愕然", "愣住", "傻眼", "呆住", "惊奇", "惊呆"]
63
63
  });
64
+ // v3.5.0 M1:情感词表构建去重(欣慰/惊惶等同表重复——双计残留)
64
65
  const EMOTION_WORDS = Object.freeze({
65
- joy: [...STRONG_EMOTION_WORDS.joy, ...WEAK_EMOTION_WORDS.joy],
66
- anger: [...STRONG_EMOTION_WORDS.anger, ...WEAK_EMOTION_WORDS.anger],
67
- sorrow: [...STRONG_EMOTION_WORDS.sorrow, ...WEAK_EMOTION_WORDS.sorrow],
68
- fear: [...STRONG_EMOTION_WORDS.fear, ...WEAK_EMOTION_WORDS.fear],
69
- surprise: [...STRONG_EMOTION_WORDS.surprise, ...WEAK_EMOTION_WORDS.surprise]
66
+ joy: [...new Set([...STRONG_EMOTION_WORDS.joy, ...WEAK_EMOTION_WORDS.joy])],
67
+ anger: [...new Set([...STRONG_EMOTION_WORDS.anger, ...WEAK_EMOTION_WORDS.anger])],
68
+ sorrow: [...new Set([...STRONG_EMOTION_WORDS.sorrow, ...WEAK_EMOTION_WORDS.sorrow])],
69
+ fear: [...new Set([...STRONG_EMOTION_WORDS.fear, ...WEAK_EMOTION_WORDS.fear])],
70
+ surprise: [...new Set([...STRONG_EMOTION_WORDS.surprise, ...WEAK_EMOTION_WORDS.surprise])]
70
71
  });
71
72
 
72
73
  /** v1.5.0 情绪污染源词表:检测到高密度时降低情感可信度并提示 AI 复核。 */
@@ -364,13 +365,24 @@ function emotionOf(text) {
364
365
  const seenWords = new Set();
365
366
  for (const list of Object.values(words)) for (const w of list) seenWords.add(w);
366
367
  dutirEmotion(""); // 确保 dutirLookup 已构建(懒加载)
367
- const bigrams = text.match(/[\u4e00-\u9fa5]{2}/g) || [];
368
+ // v3.7.0 高4:重叠滑窗提取二字组(非重叠会漏检——"他觉得一阵恶心。"奇偶偏移丢"恶心")
369
+ const bigrams = [];
370
+ for (let bi = 0; bi + 1 < text.length; bi += 1) {
371
+ const bw = text.slice(bi, bi + 2);
372
+ if (/[\u4e00-\u9fa5]/.test(bw[0]) && /[\u4e00-\u9fa5]/.test(bw[1])) bigrams.push(bw);
373
+ }
368
374
  for (const w of new Set(bigrams)) {
369
375
  if (seenWords.has(w)) continue;
370
376
  const emo = dutirLookup.get(w);
371
377
  if (!emo) continue;
372
- scores[emo] += 1;
373
- cleanScores[emo] += 1;
378
+ // v3.7.0 ⑥:DUTIR 兜底同样做否定过滤("不害怕"不再计 fear——emotionOf 有过滤,兜底漏了)
379
+ // v3.7.0 ④:按实际出现次数计(与主词表口径一致——"恶心恶心恶心"计 3 而非 1);否定只过滤首次位置前一字(接受)
380
+ const hitAt = text.indexOf(w);
381
+ if (hitAt > 0 && EMOTION_NEGATORS.has(text[hitAt - 1])) continue;
382
+ const nOccur = (text.split(w).length - 1);
383
+ if (nOccur === 0) continue;
384
+ scores[emo] += nOccur; // v3.7.0 ④:按出现次数(与主词表口径一致)
385
+ // v3.7.0 高5:DUTIR 27k 词无强弱分级(呵呵/傻乐等口语弱词)——只进 raw,不进 clean(clean 保持小词表强词口径,"净化后情感"承诺不失效)
374
386
  words[emo].push(w);
375
387
  seenWords.add(w);
376
388
  }
@@ -444,29 +456,27 @@ export function buildGuidance(ratios, topPatterns, chapterSequences) {
444
456
  */
445
457
  const VALENCE_WORDS = Object.freeze({
446
458
  "欣喜": 0.9, "狂喜": 0.9, "欢天喜地": 0.9, "雀跃": 0.7, "高兴": 0.7, "开心": 0.7, "快乐": 0.7,
447
- "喜悦": 0.7, "愉快": 0.5, "欢喜": 0.7, "兴奋": 0.7, "愉悦": 0.5, "欢快": 0.7, "": 0.5,
448
- "": 0.5, "": 0.5, "笑": 0.3, "微笑": 0.3, "笑容": 0.3, "笑眯眯": 0.3, "哈哈": 0.3,
449
- "欣慰": 0.5, "满足": 0.3, "痛快": 0.5, "爽快": 0.3, "甜": 0.5, "甜蜜": 0.5, "幸福": 0.9,
459
+ "喜悦": 0.7, "愉快": 0.5, "欢喜": 0.7, "兴奋": 0.7, "愉悦": 0.5, "欢快": 0.7, "微笑": 0.3, "笑容": 0.3, "笑眯眯": 0.3, "哈哈": 0.3,
460
+ "欣慰": 0.5, "满足": 0.3, "痛快": 0.5, "爽快": 0.3, "甜蜜": 0.5, "幸福": 0.9,
450
461
  "美满": 0.9, "温馨": 0.5, "温暖": 0.3, "踏实": 0.1, "平静": 0.1, "释然": 0.4, "解脱": 0.4,
451
- "喜欢": 0.7, "喜爱": 0.7, "欣赏": 0.5, "爱": 0.9, "疼爱": 0.7, "宠爱": 0.7, "仰慕": 0.7,
462
+ "喜欢": 0.7, "喜爱": 0.7, "欣赏": 0.5, "疼爱": 0.7, "宠爱": 0.7, "仰慕": 0.7,
452
463
  "尊敬": 0.5, "敬仰": 0.7, "崇拜": 0.7, "心动": 0.5, "眷恋": 0.7, "依恋": 0.7, "思念": 0.3,
453
464
  "心疼": 0.3, "怜爱": 0.5, "温柔": 0.3, "珍惜": 0.5, "信赖": 0.5, "感恩": 0.7,
454
465
  "愤怒": -0.9, "暴怒": -0.9, "怒发冲冠": -0.9, "火冒三丈": -0.9, "恼羞成怒": -0.9,
455
- "生气": -0.7, "恼火": -0.7, "气愤": -0.7, "怒": -0.7, "恨": -0.9, "怨恨": -0.9, "憎恨": -0.9,
456
- "厌恶": -0.7, "憎恶": -0.9, "不满": -0.5, "恼": -0.5, "气": -0.3, "发火": -0.7, "咬牙": -0.3,
466
+ "生气": -0.7, "恼火": -0.7, "气愤": -0.7, "怨恨": -0.9, "憎恨": -0.9,
467
+ "厌恶": -0.7, "憎恶": -0.9, "不满": -0.5, "发火": -0.7, "咬牙": -0.3,
457
468
  "怒意": -0.7, "怒火": -0.9, "愤恨": -0.9, "恼羞": -0.7, "气冲冲": -0.7, "咬牙切齿": -0.5,
458
469
  "悲伤": -0.7, "悲痛": -0.9, "悲痛欲绝": -0.9, "悲哀": -0.7, "哀伤": -0.7, "难过": -0.5,
459
- "伤心": -0.7, "痛苦": -0.7, "心碎": -0.9, "绝望": -0.9, "哭泣": -0.7, "": -0.5, "": -0.3,
460
- "眼泪": -0.3, "流泪": -0.5, "哽咽": -0.5, "抽泣": -0.5, "叹息": -0.3, "叹气": -0.3,
470
+ "伤心": -0.7, "痛苦": -0.7, "心碎": -0.9, "绝望": -0.9, "哭泣": -0.7, "眼泪": -0.3, "流泪": -0.5, "哽咽": -0.5, "抽泣": -0.5, "叹息": -0.3, "叹气": -0.3,
461
471
  "惆怅": -0.5, "失落": -0.5, "忧伤": -0.5, "黯然": -0.5, "心酸": -0.7, "辛酸": -0.7,
462
- "悲": -0.7, "凄凉": -0.7, "苦涩": -0.7, "苦闷": -0.5, "沮丧": -0.7, "消沉": -0.7,
472
+ "凄凉": -0.7, "苦涩": -0.7, "苦闷": -0.5, "沮丧": -0.7, "消沉": -0.7,
463
473
  "落寞": -0.5, "孤寂": -0.5, "郁闷": -0.3, "低落": -0.3, "压抑": -0.5, "心灰意冷": -0.9,
464
474
  "恐惧": -0.9, "害怕": -0.7, "惊慌": -0.7, "不安": -0.3, "紧张": -0.3, "担心": -0.3,
465
475
  "畏惧": -0.7, "惊恐": -0.9, "胆怯": -0.5, "发抖": -0.3, "哆嗦": -0.3, "心慌": -0.5,
466
476
  "毛骨悚然": -0.9, "冷汗": -0.5, "忐忑": -0.5, "惶恐": -0.9, "心悸": -0.5, "惊惶": -0.7,
467
477
  "胆战心惊": -0.9, "心虚": -0.5, "焦虑": -0.5, "恐慌": -0.9,
468
- "恶心": -0.7, "厌恶": -0.7, "鄙视": -0.7, "轻蔑": -0.5, "嫌弃": -0.7, "反感": -0.5,
469
- "憎恶": -0.9, "作呕": -0.7,
478
+ "恶心": -0.7, "鄙视": -0.7, "轻蔑": -0.5, "嫌弃": -0.7, "反感": -0.5,
479
+ "作呕": -0.7,
470
480
  "惊讶": -0.1, "震惊": -0.5, "意外": -0.1, "吃惊": -0.3, "诧异": -0.3, "愕然": -0.3,
471
481
  "愣住": -0.1, "目瞪口呆": -0.5, "难以置信": -0.5, "不可思议": -0.3, "惊愕": -0.5,
472
482
  "惊奇": 0.1, "震撼": -0.3, "傻眼": -0.3, "呆住": -0.1, "惊呆": -0.5, "骇然": -0.5
@@ -603,9 +613,11 @@ export function implicitEmotionScan(text, semResolver = null) {
603
613
  else { posHits += n * valence; if (false) fragileHits += n; }
604
614
  carrierCounts.set(label + ":" + word, (carrierCounts.get(label + ":" + word) ?? 0) + n);
605
615
  };
606
- // ① 单方向表(原有)
616
+ // ① 单方向表(原有)——v3.5.0 M2:跳过与多方向表重叠的词(雨/黄昏/烛火等),防同一出现计 2-3 次(语境裁决交给 ②)
617
+ const ambWords = new Set(Object.keys(AMBIGUOUS_CARRIERS));
607
618
  for (const carrier of IMPLICIT_CARRIERS) {
608
619
  for (const word of carrier.words) {
620
+ if (ambWords.has(word)) continue;
609
621
  const n = text.split(word).length - 1;
610
622
  if (n > 0) {
611
623
  see(carrier.label, word, n, carrier.valence);
@@ -683,7 +695,8 @@ export function valenceSeries(text, winChars = 100) {
683
695
  from = idx + word.length;
684
696
  }
685
697
  }
686
- series.push(n === 0 ? 0 : Math.round((sum / n) * 1000) / 1000);
698
+ // v3.7.0 高6:零命中窗口(无情感词)不进 series——0 会稀释均值并伪造"趋势回升"信号
699
+ if (n > 0) series.push(Math.round((sum / n) * 1000) / 1000);
687
700
  windowPosNeg.push({ pos, neg });
688
701
  }
689
702
  const posWords = windowPosNeg.reduce((s, w) => s + w.pos, 0);
@@ -696,9 +709,11 @@ export function valenceStats(text) {
696
709
  // v3.5.0 #57:一次 valenceSeries 取全部(旧版窗口统计二次调用,大书白扫一遍词表)
697
710
  const { series, posWords, negWords, windowPosNeg } = valenceSeries(text);
698
711
  const n = series.length;
699
- if (n === 0) return { windows: 0 };
712
+ // v3.7.0:零命中窗口跳过后的空系列——补全字段(下游 emotion.quantification.stats 恒有值)
713
+ if (n === 0) return { windows: 0, variance: 0, adjVariance: 0, delta: 0, deltaRobust: 0, conflict: 0, posRatio: 0, negRatio: 0, meanValence: 0 };
700
714
  const mean = series.reduce((x, y) => x + y, 0) / n;
701
- const variance = series.reduce((s, x) => s + (x - mean) ** 2, 0) / n;
715
+ // v3.6.0:样本方差口径 /(n-1)(n=1 时无方差=0
716
+ const variance = n > 1 ? series.reduce((s, x) => s + (x - mean) ** 2, 0) / (n - 1) : 0;
702
717
  let adjSum = 0;
703
718
  for (let i = 1; i < n; i += 1) adjSum += Math.abs(series[i] - series[i - 1]);
704
719
  const adjVariance = n > 1 ? adjSum / (n - 1) : 0;
@@ -741,7 +756,8 @@ export function explicitImplicitCompare(explicitMean, implicit) {
741
756
  const explicitSign = explicitMean > 0.15 ? "positive" : explicitMean < -0.15 ? "negative" : "neutral";
742
757
  const implicitSign = implicit.negative >= 0.6 ? "negative" : implicit.positive >= 0.6 ? "positive" : "neutral";
743
758
  return {
744
- explicitImplicitConflict: explicitSign === "positive" && implicitSign === "negative",
759
+ // v3.7.0 引擎⑦:双向冲突(显负+隐正也报——原只查显正+隐负)
760
+ explicitImplicitConflict: (explicitSign === "positive" && implicitSign === "negative") || (explicitSign === "negative" && implicitSign === "positive"),
745
761
  explicitSign,
746
762
  implicitSign
747
763
  };
@@ -867,6 +883,8 @@ export function emotionalQuantification(text, perChapter, blocks, semResolver =
867
883
  * @returns 结构化分析结果(与 novel_sentence_analysis 输出 schema 一致)。
868
884
  */
869
885
  export function analyzeText(text, options = {}) {
886
+ // v3.5.0 M6:入口容错(null/undefined 不崩溃)
887
+ text = String(text ?? "");
870
888
  const top = Number.isInteger(options.top) ? options.top : 8;
871
889
  // v3.5.0 #63:maxSentences 钳制 ≥1(0/负数会让 slice(0,-5) 产生错误语义)
872
890
  const maxSentences = Math.max(1, Number.isInteger(options.maxSentences) ? options.maxSentences : 20000);
@@ -1030,7 +1048,9 @@ export function analyzeText(text, options = {}) {
1030
1048
  }
1031
1049
  for (const [emotion, words] of Object.entries(sentence.emotion.words)) {
1032
1050
  for (const word of words) {
1033
- emotionWordCounts.set(word, (emotionWordCounts.get(word) ?? 0) + 1);
1051
+ // v3.7.0 引擎③:按文本内实际出现次数计数("开心开心开心"不再 count=1
1052
+ const nInSentence = sentence.text ? (sentence.text.split(word).length - 1) : 1;
1053
+ emotionWordCounts.set(word, (emotionWordCounts.get(word) ?? 0) + nInSentence);
1034
1054
  }
1035
1055
  }
1036
1056
  }
@@ -1067,7 +1087,8 @@ export function analyzeText(text, options = {}) {
1067
1087
  if (r18Density >= 1.5) pollutedBy.push("高密度 R18/生理描写(每千字 " + r18Density + ")");
1068
1088
  if (battleDensity >= 3) pollutedBy.push("高密度战斗/爽文描写(每千字 " + battleDensity + ")");
1069
1089
  if (horrorDensity >= 3) pollutedBy.push("高密度恐怖/疯狂描写(每千字 " + horrorDensity + ")");
1070
- const confidence = polluted ? "low" : (cleanDominantEmotion !== "neutral" && cleanDominantEmotion !== dominantEmotion ? "medium" : "high");
1090
+ // v3.7.0 引擎②:clean 无强词主导(全来自弱词)→ medium(不再判 high
1091
+ const confidence = polluted ? "low" : (cleanDominantEmotion === "neutral" ? "medium" : (cleanDominantEmotion !== dominantEmotion ? "medium" : "high"));
1071
1092
  const caveat = polluted
1072
1093
  ? "⚠️ 检测到" + pollutedBy.join("、") + ",dominant(" + EMOTION_LABELS[dominantEmotion] + ")可能来自生理/爽感反应词而非真实情感。请勿直接采信,须 novel_read 抽查 2-3 段原文复核真实情感基调后再下结论。"
1073
1094
  : (cleanDominantEmotion !== dominantEmotion
@@ -1098,19 +1119,43 @@ export function analyzeText(text, options = {}) {
1098
1119
  for (const [emotionName, words] of Object.entries(EMOTION_WORDS)) {
1099
1120
  if (words.includes(word)) { emotionOfWord = emotionName; break; }
1100
1121
  }
1122
+ // v3.7.0 引擎④:DUTIR 兜底词标出真实情感(不再与 scores 自相矛盾标 neutral)
1123
+ if (emotionOfWord === "neutral") emotionOfWord = dutirLookup.get(word) || "neutral";
1101
1124
  return { word, count, emotion: emotionOfWord };
1102
1125
  })
1103
1126
  .sort((a, b) => b.count - a.count || a.word.localeCompare(b.word))
1104
1127
  .slice(0, 12),
1105
1128
  curve: emotionCurve(blockMeta, curveSegments),
1106
1129
  // v1.6.0:情感量化(Valence 三指标 + 显隐对比 + 复杂度 + 复合共现)
1107
- quantification: emotionalQuantification(text, [], blockMeta.map((m) => m.sentences).filter((s) => s.length > 0), options.semResolver || null)
1130
+ quantification: emotionalQuantification(text, blockMeta.map(function (m, bi) {
1131
+ // v3.7.0 引擎⑤:perChapter 用段级分组(此前硬编码 [] → chapterDrift.swinging 恒 false)
1132
+ const pcCounts = { joy: 0, anger: 0, sorrow: 0, fear: 0, surprise: 0 };
1133
+ for (const s of m.sentences) {
1134
+ const cs = s.emotion.cleanScores ?? {};
1135
+ for (const k of Object.keys(pcCounts)) pcCounts[k] += cs[k] ?? 0;
1136
+ }
1137
+ return { chapter: "段" + (bi + 1), counts: pcCounts };
1138
+ }).filter((m) => Object.values(m.counts).some((v) => v > 0)), blockMeta.map((m) => m.sentences).filter((s) => s.length > 0), options.semResolver || null)
1108
1139
  };
1109
1140
 
1110
1141
  // 主观性指数(启发式 0-100)
1111
1142
  const psychRatio = totalSentences === 0 ? 0 : counts.psychology / totalSentences;
1112
1143
  const exclaimRatio = totalSentences === 0 ? 0 : counts.exclamation / totalSentences;
1113
- const firstPersonCount = FIRST_PERSON_WORDS.reduce((sum, word) => sum + (text.split(word).length - 1), 0);
1144
+ // v3.6.0:第一人称最长匹配优先("我们"不拆成 我+我们 双计)
1145
+ const firstPersonCount = (function () {
1146
+ let fpSum = 0;
1147
+ const fpSorted = FIRST_PERSON_WORDS.slice().sort((x, y) => y.length - x.length);
1148
+ let fpText = text;
1149
+ for (const w of fpSorted) {
1150
+ if (w.length < 2) continue;
1151
+ const parts = fpText.split(w);
1152
+ fpSum += parts.length - 1;
1153
+ fpText = parts.join("\u0000".repeat(w.length));
1154
+ }
1155
+ // 剩余孤立单字"我"
1156
+ fpSum += (fpText.match(/我/g) || []).length;
1157
+ return fpSum;
1158
+ })();
1114
1159
  const firstPersonDensity = totalChars === 0 ? 0 : (firstPersonCount / totalChars) * 1000;
1115
1160
  const subjectivityIndex = Math.min(100, Math.round(
1116
1161
  psychRatio * 50 + exclaimRatio * 60 + Math.min(emotionDensity * 2.5, 25) + Math.min(firstPersonDensity * 1.2, 20)
@@ -1204,6 +1249,8 @@ function emotionCurve(blockMeta, maxSegments) {
1204
1249
  intensity: chars === 0 ? 0 : round((total / chars) * 1000, 2)
1205
1250
  });
1206
1251
  }
1252
+ // v3.6.0 复审修正:不裁剪——强度 0 段是"平缓段"(跨章节曲线对齐需要固定段数契约);
1253
+ // segmentCount=min(blocks, maxSegments) 已保证每段至少 1 块,无空 slice
1207
1254
  return curve;
1208
1255
  }
1209
1256
 
@@ -1330,7 +1377,7 @@ function loadDutir() {
1330
1377
  }
1331
1378
  return DUTIR;
1332
1379
  }
1333
- const DUTIR_TO_EMOTION = { 乐: "joy", 好: "joy", 怒: "anger", 哀: "sorrow", 惧: "fear", 恶: "sorrow", 惊: "surprise" };
1380
+ const DUTIR_TO_EMOTION = { 乐: "joy", 好: "joy", 怒: "anger", 哀: "sorrow", 惧: "fear", 恶: "anger", 惊: "surprise" }; // v3.5.0 H4:恶(厌恶) 归 anger 而非 sorrow
1334
1381
  const dutirLookup = new Map();
1335
1382
  function dutirEmotion(word) {
1336
1383
  if (dutirLookup.size === 0) {
@@ -1338,7 +1385,8 @@ function dutirEmotion(word) {
1338
1385
  for (const [cat, words] of Object.entries(dutir)) {
1339
1386
  const emo = DUTIR_TO_EMOTION[cat];
1340
1387
  if (!emo) continue;
1341
- for (const w of words) if (typeof w === "string" && w.length >= 2) dutirLookup.set(w, emo);
1388
+ // v3.5.0 H4:跨类冲突词首见保留(后写不再覆盖——"开心"不会被 类覆盖成 sorrow)
1389
+ for (const w of words) if (typeof w === "string" && w.length >= 2 && !dutirLookup.has(w)) dutirLookup.set(w, emo);
1342
1390
  }
1343
1391
  }
1344
1392
  return dutirLookup.get(word);
package/lib/client.js CHANGED
@@ -16,6 +16,7 @@ window.__ModuleLoader__.load({
16
16
  var module = { exports: {} };
17
17
  // v3.5.0 R1:rawTimer 必须在 createController 闭包外/顶部声明(旧位置在渲染函数内,api 方法访问不到 → ReferenceError)
18
18
  var rawTimer = null;
19
+ var reportReqId = 0; // v3.5.0 M15:模块级请求序号(渲染闭包变量每次渲染归零,守卫失效)
19
20
  var exports = module.exports;
20
21
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
21
22
  let react = require("react");
@@ -213,7 +214,6 @@ var css = `
213
214
  "raw.promiseText": "请输入以下承诺以开启(必须包含「绝不传播」):\n\n我承诺开启此功能仅用于个人创作与研究,绝不传播。",
214
215
  "raw.promisePlaceholder": "我承诺开启此功能仅用于个人创作与研究,绝不传播",
215
216
  "raw.promiseOk": "确认",
216
- "feature.semanticImplicit.desc": "25 个情感原型句扫全书,找词表外疑似意象段落(semanticImplicit)。",
217
217
  "model.title": "小模型(本地语义引擎)",
218
218
  "model.manage": "管理",
219
219
  "model.hint": "总开关:语义增强。以下子功能独立开关,默认全开;关闭总开关后以下全部失效。",
@@ -336,7 +336,23 @@ var css = `
336
336
  "creation.deleted": "已删除该书设定",
337
337
  "creation.newed": "已创建设定,开始填写",
338
338
  "creation.needName": "请先输入新书书名",
339
+ "creation.badName": "书名含非法字符(\\ / : * ? \" < > | 或结尾点/空格/系统保留名)",
340
+ "reports.loading": "读取中…",
339
341
  "stats.pending": "统计未就绪(宿主未提供,点刷新重试)",
342
+ "tol.recommend": "推荐",
343
+ "tol.halfFilled": "维度",
344
+ "tol.halfFilledTail": "」只填了低/高一边——容差需成对填写(或都留空用推荐)",
345
+ "state.unreachable": "宿主端不可达",
346
+ "reports.readFail": "读取失败",
347
+ "reports.readFailDetail": "网络/路由不可用",
348
+ "reports.loading2": "加载中…",
349
+ "demo.fail": "演示失败",
350
+ "ph.e末": "例如:末日后异能世界…",
351
+ "ph.e主": "例如:寻找失踪的同伴,解开身世之谜…",
352
+ "ph.e女": "例如:女主冷静克制,有秘密…",
353
+ "ph.e不": "例如:不能有重生/系统/金手指…",
354
+ "ph.e悬": "例如:悬疑+救赎,都市背景…",
355
+ "ph.e每": "例如:每章结尾留钩子,多对话…",
340
356
  "stats.empty": "书库为空——在 novels/ 放一本小说,或让 AI 原创一本",
341
357
  "stats.title": "书库统计",
342
358
  "stats.week": "7天",
@@ -403,7 +419,7 @@ var css = `
403
419
  "raw.promiseText": "Type the following promise to enable (must contain \"never distribute\"):\n\nI promise to use this feature for personal creation and research only, and never distribute it.",
404
420
  "raw.promisePlaceholder": "I promise to use this only for personal creation and research, never distribute",
405
421
  "raw.promiseOk": "Confirm",
406
- "feature.semanticImplicit.desc": "25 emotion prototypes scan the book for implicit imagery paragraphs.",
422
+ "feature.semanticImplicit.desc": "29 emotion prototypes scan the book for implicit imagery paragraphs.",
407
423
  "model.title": "Local model (semantic engine)",
408
424
  "model.manage": "Manage",
409
425
  "model.hint": "Master switch: semantic enhancement. Sub-switches below are independent; disabling the master disables all.",
@@ -418,6 +434,10 @@ var css = `
418
434
  "toolGroup.analyze": "📊 Analysis",
419
435
  "toolGroup.settings": "📚 Settings",
420
436
  "toolGroup.create": "✍️ Creation",
437
+ "toolGroup.analyze.desc": "Browse books, keywords, sentence/emotion analysis, style check, semantic search, continuity audit.",
438
+ "toolGroup.settings.desc": "Characters, locations, items, timeline, plot tracking, summaries and import — your story's database.",
439
+ "toolGroup.create.desc": "New chapters and switch configuration.",
440
+ "tool.novel_semantic_search.desc": "Semantic search (local embedding, 0 token): natural-language retrieval across the book.",
421
441
  "group.allOn": "All on",
422
442
  "group.allOff": "All off",
423
443
  "group.onCount": "{n} on",
@@ -522,7 +542,23 @@ var css = `
522
542
  "creation.deleted": "Profile deleted",
523
543
  "creation.newed": "Profile created, start filling",
524
544
  "creation.needName": "Please enter a book name first",
545
+ "creation.badName": "Invalid book name (\\ / : * ? \" < > | or trailing dot/space / reserved name)",
546
+ "reports.loading": "Loading…",
525
547
  "stats.pending": "Stats pending (host unavailable — press refresh)",
548
+ "tol.recommend": "recommended",
549
+ "tol.halfFilled": "Dimension",
550
+ "tol.halfFilledTail": " — fill both low & high (or leave both empty to use recommended)",
551
+ "state.unreachable": "host unreachable",
552
+ "reports.readFail": "Failed to read",
553
+ "reports.readFailDetail": "network/route unavailable",
554
+ "reports.loading2": "Loading…",
555
+ "demo.fail": "Demo failed",
556
+ "ph.e末": "e.g. post-apocalyptic world with abilities…",
557
+ "ph.e主": "e.g. find the missing companion and unravel the mystery of her origins…",
558
+ "ph.e女": "e.g. cool-headed heroine with secrets…",
559
+ "ph.e不": "e.g. no rebirth/system/golden finger…",
560
+ "ph.e悬": "e.g. suspense + redemption, urban setting…",
561
+ "ph.e每": "e.g. cliffhanger endings, dialogue-heavy…",
526
562
  "stats.empty": "Library empty — put a novel in novels/, or ask AI to create one",
527
563
  "stats.title": "Library stats",
528
564
  "stats.week": "/7d",
@@ -642,8 +678,9 @@ var css = `
642
678
  var revAt = api.getSnapshot().rev;
643
679
  return fetchState().then(function (remote) {
644
680
  api.set({
645
- enabled: !!remote.enabled,
646
- autoAnalyze: !!remote.autoAnalyze,
681
+ // v3.7.0 ②:enabled/autoAnalyze 与 tools/features 同样受竞态守卫(慢请求不覆盖用户刚切的总开关)
682
+ enabled: (api.getSnapshot().rev === revAt ? !!remote.enabled : api.getSnapshot().enabled),
683
+ autoAnalyze: (api.getSnapshot().rev === revAt ? !!remote.autoAnalyze : api.getSnapshot().autoAnalyze),
647
684
  // v3.5.0 R4:请求期间版本未变(用户没操作)才用远端覆盖开关——慢请求不覆盖用户刚切的
648
685
  tools: (api.getSnapshot().rev === revAt ? (remote.tools || {}) : api.getSnapshot().tools || {}),
649
686
  features: (api.getSnapshot().rev === revAt ? (remote.features || {}) : api.getSnapshot().features || {}),
@@ -667,7 +704,7 @@ var css = `
667
704
  });
668
705
  return remote;
669
706
  }).catch(function () {
670
- api.set({ hostOk: false, loading: false, refreshing: false, revealMsg: t("plot.revealErr") + ":宿主端不可达", revealErr: true });
707
+ api.set({ hostOk: false, loading: false, refreshing: false, revealMsg: t("plot.revealErr") + "" + t("state.unreachable"), revealErr: true });
671
708
  return null;
672
709
  });
673
710
  },
@@ -687,7 +724,7 @@ var css = `
687
724
  api.set({ revealMsg: data.error || t("plot.revealErr"), revealErr: true });
688
725
  }
689
726
  }).catch(function () {
690
- api.set({ revealMsg: t("plot.revealErr") + ":宿主端不可达", revealErr: true });
727
+ api.set({ revealMsg: t("plot.revealErr") + "" + t("state.unreachable"), revealErr: true });
691
728
  }).finally(function () {
692
729
  api.set({ revealing: false });
693
730
  });
@@ -823,6 +860,8 @@ var css = `
823
860
  syncActive();
824
861
  tryPlace();
825
862
  return function () {
863
+ // v3.7.0 ⑤:卸载时清理 rawTimer 倒计时(防 interval 泄漏)
864
+ if (rawTimer) { clearInterval(rawTimer); rawTimer = null; }
826
865
  waitObserver.disconnect();
827
866
  rootObserver.disconnect();
828
867
  unsubscribe();
@@ -849,6 +888,8 @@ var css = `
849
888
  );
850
889
  }
851
890
  function PanelView(props) {
891
+ // v3.7.0 高1:isEn 声明在视图作用域(此前只在 rawSetPromise 内部,基线视图引用 ReferenceError 必崩)
892
+ var isEn = typeof document !== "undefined" && (document.documentElement.lang || "zh").toLowerCase().startsWith("en");
852
893
  var force = react.useState(0)[1];
853
894
  react.useEffect(function () {
854
895
  return props.controller.subscribe(function () { force(function (n) { return n + 1; }); });
@@ -871,7 +912,11 @@ var css = `
871
912
  );
872
913
  };
873
914
  var backBtn = function () {
874
- return el("button", { type: "button", className: "nwBackBtn", onClick: function () { props.controller.openView(view === "reports" || view === "creation" ? "main" : view === "baseline" ? "main" : view === "creation-form" ? "creation" : view === "model" ? "features" : "main"); } }, "‹ " + t("panel.back"));
915
+ return el("button", { type: "button", className: "nwBackBtn", onClick: function () {
916
+ // v3.7.0 ④:creation-form 返回前 dirty 确认(与 switchBook 同机制,防静默丢修改)
917
+ if (view === "creation-form" && state.creationDirty && !window.confirm(t("creation.dirtyWarn"))) return;
918
+ props.controller.openView(view === "reports" || view === "creation" ? "main" : view === "baseline" ? "main" : view === "creation-form" ? "creation" : view === "model" ? "features" : "main");
919
+ } }, "‹ " + t("panel.back"));
875
920
  };
876
921
  var switchRow = function (name, fOn, onToggle, extra) {
877
922
  return el("div", { className: "nwToolRow" + (fOn ? " nwToolRowOn" : ""), key: name },
@@ -1056,8 +1101,8 @@ var TOOL_GROUPS = [
1056
1101
  // v3.0.0:风格基线——六维文笔指标 ±% 容差带(左=允许低于,右=允许高于)
1057
1102
  var DEFAULT_TOL = { low: "", high: "" };
1058
1103
  var NW_METRICS = [
1059
- ["complexity", "句法复杂度", "📐"], ["modifierDensity", "修饰密度", "🎨"], ["abstractDensity", "抽象度", "☁️"],
1060
- ["actionDensity", "动作密度", "⚡"], ["hedgeDensity", "不确定性", "🌫️"], ["gapIndex", "留白指数", "🕳️"]
1104
+ ["complexity", "句法复杂度", "📐", "Syntax"], ["modifierDensity", "修饰密度", "🎨", "Modifier"], ["abstractDensity", "抽象度", "☁️", "Abstract"],
1105
+ ["actionDensity", "动作密度", "⚡", "Action"], ["hedgeDensity", "不确定性", "🌫️", "Hedge"], ["gapIndex", "留白指数", "🕳️", "Gap"]
1061
1106
  ];
1062
1107
  // v3.0.0:输入框留空 = 使用推荐容差(原著章节波动 1.5σ);填了才保存自定义
1063
1108
  var tolState = state.styleTolerance && typeof state.styleTolerance === "object" ? state.styleTolerance : null;
@@ -1091,13 +1136,19 @@ var TOOL_GROUPS = [
1091
1136
  setTimeout(function () { props.controller.set({ baselineSaved: false }); }, 1800);
1092
1137
  };
1093
1138
  var saveTol = function () {
1094
- // 只保存填写了低+高的维度;全空 → null(全部用推荐)
1139
+ // v3.5.0 M18:单侧填写提示成对(不再静默丢弃)
1140
+ var halfFilled = null;
1095
1141
  var tol = {};
1096
1142
  var any = false;
1097
1143
  for (var si = 0; si < NW_METRICS.length; si += 1) {
1098
1144
  var sk = NW_METRICS[si][0];
1099
1145
  var sd = draft[sk];
1100
1146
  if (sd && sd.low !== "" && sd.high !== "") { tol[sk] = { low: -sd.low, high: sd.high }; any = true; }
1147
+ else if (sd && (sd.low !== "" || sd.high !== "")) { halfFilled = halfFilled || sk; }
1148
+ }
1149
+ if (halfFilled) {
1150
+ props.controller.set({ revealMsg: t("tol.halfFilled") + "「" + halfFilled + "」" + t("tol.halfFilledTail"), revealErr: true });
1151
+ return;
1101
1152
  }
1102
1153
  props.toggle({ styleTolerance: any ? tol : null });
1103
1154
  props.controller.set({ baselineDraft: null, revealMsg: t("baseline.saved"), revealErr: false });
@@ -1119,11 +1170,11 @@ var TOOL_GROUPS = [
1119
1170
  var mk2 = item[0];
1120
1171
  var d = draft[mk2] || DEFAULT_TOL;
1121
1172
  return el("div", { className: "nwTolRow", key: mk2, style: { display: "flex", alignItems: "center", gap: "10px", padding: "9px 2px" } },
1122
- el("div", { className: "nwTolName", style: { flex: "1", minWidth: "0", display: "flex", alignItems: "center", gap: "8px", fontSize: "13px", fontWeight: "500", whiteSpace: "nowrap", color: "#334155" } }, el("span", { className: "nwTolIcon", style: { fontSize: "15px" } }, item[2]), item[1]),
1173
+ el("div", { className: "nwTolName", style: { flex: "1", minWidth: "0", display: "flex", alignItems: "center", gap: "8px", fontSize: "13px", fontWeight: "500", whiteSpace: "nowrap", color: "#334155" } }, el("span", { className: "nwTolIcon", style: { fontSize: "15px" } }, item[2]), isEn ? (item[3] || item[1]) : item[1]),
1123
1174
  el("div", { className: "nwTolField", style: { width: "74px", flexShrink: "0", display: "flex", flexDirection: "column", gap: "3px" } },
1124
1175
  el("div", { style: { display: "flex", alignItems: "center", gap: "4px" } },
1125
1176
  el("span", { className: "nwTolSign", style: { width: "12px", flexShrink: "0", textAlign: "center", fontSize: "14px", color: "#ef4444", fontWeight: "600" } }, "−"),
1126
- el("input", { type: "number", className: "nwTolInput", placeholder: "推荐", value: d.low, min: 0, max: 100, style: { width: "100%", height: "30px", boxSizing: "border-box", border: "1px solid #d4d4d8", borderRadius: "8px", fontSize: "13px", textAlign: "center", background: "#fafafa", color: "#18181b", outline: "none", MozAppearance: "textfield" }, onChange: function (ev) { setDraft(mk2, "low", ev.target.value); } })
1177
+ el("input", { type: "number", className: "nwTolInput", placeholder: t("tol.recommend"), value: d.low, min: 0, max: 100, style: { width: "100%", height: "30px", boxSizing: "border-box", border: "1px solid #d4d4d8", borderRadius: "8px", fontSize: "13px", textAlign: "center", background: "#fafafa", color: "#18181b", outline: "none", MozAppearance: "textfield" }, onChange: function (ev) { setDraft(mk2, "low", ev.target.value); } })
1127
1178
  ),
1128
1179
  el("span", { className: "nwTolCaption", style: { fontSize: "10px", color: "#94a3b8", textAlign: "center" } }, t("baseline.low"))
1129
1180
  ),
@@ -1131,7 +1182,7 @@ var TOOL_GROUPS = [
1131
1182
  el("div", { className: "nwTolField", style: { width: "74px", flexShrink: "0", display: "flex", flexDirection: "column", gap: "3px" } },
1132
1183
  el("div", { style: { display: "flex", alignItems: "center", gap: "4px" } },
1133
1184
  el("span", { className: "nwTolSign", style: { width: "12px", flexShrink: "0", textAlign: "center", fontSize: "14px", color: "#10b981", fontWeight: "600" } }, "+"),
1134
- el("input", { type: "number", className: "nwTolInput", placeholder: "推荐", value: d.high, min: 0, max: 100, style: { width: "100%", height: "30px", boxSizing: "border-box", border: "1px solid #d4d4d8", borderRadius: "8px", fontSize: "13px", textAlign: "center", background: "#fafafa", color: "#18181b", outline: "none", MozAppearance: "textfield" }, onChange: function (ev) { setDraft(mk2, "high", ev.target.value); } })
1185
+ el("input", { type: "number", className: "nwTolInput", placeholder: t("tol.recommend"), value: d.high, min: 0, max: 100, style: { width: "100%", height: "30px", boxSizing: "border-box", border: "1px solid #d4d4d8", borderRadius: "8px", fontSize: "13px", textAlign: "center", background: "#fafafa", color: "#18181b", outline: "none", MozAppearance: "textfield" }, onChange: function (ev) { setDraft(mk2, "high", ev.target.value); } })
1135
1186
  ),
1136
1187
  el("span", { className: "nwTolCaption", style: { fontSize: "10px", color: "#94a3b8", textAlign: "center" } }, t("baseline.high"))
1137
1188
  ),
@@ -1146,14 +1197,13 @@ var TOOL_GROUPS = [
1146
1197
  );
1147
1198
  } else if (view === "reports") {
1148
1199
  // v3.2.0:报告历史(加载在入口 onClick 触发,此处纯渲染防 React 345 循环)
1149
- var reportReqId = 0;
1150
1200
  var openReport = function (file) {
1151
1201
  var reqId = ++reportReqId;
1152
- props.controller.set({ reportContent: "读取中…" });
1202
+ props.controller.set({ reportContent: t("reports.loading") });
1153
1203
  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
1204
  if (reqId !== reportReqId) return; // v3.5.0 #47:快速切换时丢弃过期响应
1155
- props.controller.set({ reportContent: d.ok ? JSON.stringify(d.content, null, 2) : ("读取失败:" + (d.error || "")) });
1156
- }).catch(function () { props.controller.set({ reportContent: "读取失败(网络/路由不可用)" }); });
1205
+ props.controller.set({ reportContent: d.ok ? JSON.stringify(d.content, null, 2) : (t("reports.readFail") + ":" + (d.error || "")) });
1206
+ }).catch(function () { props.controller.set({ reportContent: t("reports.readFailDetail") }); });
1157
1207
  };
1158
1208
  var groups = state.reportsGroups || [];
1159
1209
  var totalFiles = groups.reduce(function (a, g) { return a + (g.files || []).length; }, 0);
@@ -1168,7 +1218,7 @@ var TOOL_GROUPS = [
1168
1218
  el("pre", { style: { margin: "4px 0 0", padding: "10px", borderRadius: "8px", background: "#fff", border: "1px solid #e2e8f0", fontSize: "11px", lineHeight: "1.7", color: "#334155", whiteSpace: "pre-wrap", wordBreak: "break-all", maxHeight: "70vh", overflow: "auto" } }, state.reportContent)
1169
1219
  )
1170
1220
  : el("div", { style: { display: "flex", flexDirection: "column", gap: "8px" } },
1171
- reportsLoading ? el("div", { style: { fontSize: "12px", color: "#94a3b8", padding: "10px 0" } }, "加载中…") : totalFiles === 0 ? el("div", { style: { fontSize: "12px", color: "#94a3b8", padding: "10px 0" } }, t("reports.empty")) : null,
1221
+ reportsLoading ? el("div", { style: { fontSize: "12px", color: "#94a3b8", padding: "10px 0" } }, t("reports.loading2")) : totalFiles === 0 ? el("div", { style: { fontSize: "12px", color: "#94a3b8", padding: "10px 0" } }, t("reports.empty")) : null,
1172
1222
  groups.map(function (g) {
1173
1223
  return el("div", { key: g.name },
1174
1224
  el("div", { style: { fontSize: "12px", fontWeight: "600", color: "#475569", margin: "6px 0 4px" } }, g.name + "(" + (g.files || []).length + ")"),
@@ -1185,12 +1235,12 @@ var TOOL_GROUPS = [
1185
1235
  } else if (view === "creation" || view === "creation-form") {
1186
1236
  // v3.1.0:原创模式设定库(方案 C:列表页 + 表单页 + 快速切换)
1187
1237
  var CREATION_FIELDS = [
1188
- ["worldview", "🌍", t("creation.worldview"), "例如:末日后异能世界…"],
1189
- ["characters", "🎭", t("creation.characters"), "例如:女主冷静克制,有秘密…"],
1190
- ["forbidden", "🚫", t("creation.forbidden"), "例如:不能有重生/系统/金手指…"],
1191
- ["mainConflict", "🎯", t("creation.mainConflict"), "例如:女主寻找失踪的同伴…"],
1192
- ["genre", "📚", t("creation.genre"), "例如:悬疑+救赎,都市背景…"],
1193
- ["extra", "📝", t("creation.extra"), "例如:每章结尾留钩子,多对话…"]
1238
+ ["worldview", "🌍", t("creation.worldview"), t("ph.e末")],
1239
+ ["characters", "🎭", t("creation.characters"), t("ph.e女")],
1240
+ ["forbidden", "🚫", t("creation.forbidden"), t("ph.e不")],
1241
+ ["mainConflict", "🎯", t("creation.mainConflict"), t("ph.e主")],
1242
+ ["genre", "📚", t("creation.genre"), t("ph.e悬")],
1243
+ ["extra", "📝", t("creation.extra"), t("ph.e每")]
1194
1244
  ];
1195
1245
  var cpGlobal = state.creationProfile && typeof state.creationProfile === "object" ? state.creationProfile : {};
1196
1246
  var cpBooks = state.creationProfiles && typeof state.creationProfiles === "object" ? state.creationProfiles : {};
@@ -1230,7 +1280,7 @@ var TOOL_GROUPS = [
1230
1280
  var nb = String(newBookName || "").trim();
1231
1281
  // v3.5.0 #49:客户端校验——路径分隔符/Windows 保留字符直接提示
1232
1282
  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 });
1283
+ props.controller.set({ revealMsg: t("creation.badName"), revealErr: true });
1234
1284
  return;
1235
1285
  }
1236
1286
  if (!nb) {
@@ -1393,6 +1443,10 @@ var TOOL_GROUPS = [
1393
1443
  entry(t("panel.creationTitle"), t("panel.creationHint"), null, "creation"),
1394
1444
  // v3.0.0:风格基线(六维 ±% 容差带)
1395
1445
  entry(t("panel.baselineTitle"), t("panel.baselineHint"), null, "baseline"),
1446
+ // v3.7.0 ②:主面板功能开关(合并行:情感深度/风格检测/语义增强 + 非净化)——原注释位置,此前误渲染进 tools 子页
1447
+ el("div", { className: "nwSectionTitle" }, t("panel.featuresTitle")),
1448
+ el("div", { className: "nwToolsHint" }, t("panel.featuresHint")),
1449
+ featureRows,
1396
1450
  // v2.6.0:数据目录占用 + 语义引擎状态(一目了然)
1397
1451
  el("div", { className: "nwRow nwRowOff" },
1398
1452
  el("div", { className: "nwRowText" },
@@ -1402,7 +1456,6 @@ var TOOL_GROUPS = [
1402
1456
  ),
1403
1457
  pathBox(t("dir.data"), dirs.dataDir || state.dataDir || "", "data-dir"),
1404
1458
  // v3.2.0:书库统计(写作打卡);null=未就绪不渲染,[]=真空提示
1405
- state.booksStats === null ? null :
1406
1459
  el("div", { key: "statsCard", style: { marginTop: "10px", padding: "10px 12px", borderRadius: "10px", border: "1px solid #e2e8f0", background: "#fff" } },
1407
1460
  el("div", { style: { fontSize: "12px", fontWeight: "600", color: "#475569", marginBottom: "6px" } }, "📚 " + t("stats.title")),
1408
1461
  el("div", { style: { display: "flex", flexDirection: "column", gap: "5px" } },
@@ -1440,8 +1493,8 @@ var TOOL_GROUPS = [
1440
1493
  if (state.demoLoading) return;
1441
1494
  props.controller.set({ demoLoading: true });
1442
1495
  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) {
1443
- props.controller.set({ demoReport: d.ok ? d.report : ("演示失败:" + (d.error || "")), demoLoading: false });
1444
- }).catch(function () { props.controller.set({ demoReport: "演示失败(网络/路由不可用)", demoLoading: false }); });
1496
+ props.controller.set({ demoReport: d.ok ? d.report : (t("demo.fail") + ":" + (d.error || "")), demoLoading: false });
1497
+ }).catch(function () { props.controller.set({ demoReport: t("reports.readFailDetail"), demoLoading: false }); });
1445
1498
  } },
1446
1499
  el("span", { style: { fontSize: "13px", fontWeight: "600", color: "#6366f1" } }, state.demoLoading ? t("demo.loading") : t("demo.run")),
1447
1500
  el("span", { style: { fontSize: "11px", color: "#94a3b8" } }, t("demo.hint"))
@@ -1644,7 +1697,8 @@ var TOOL_GROUPS = [
1644
1697
  booksStats: typeof remote.booksStats === "undefined" ? null : (remote.booksStats || []),
1645
1698
  dirs: remote.dirs || null,
1646
1699
  file: remote.file || "",
1647
- source: "host",
1700
+ saveFailed: false, // v3.7.0 ③:刷新成功清除错误横幅
1701
+ source: "host",
1648
1702
  hostOk: true,
1649
1703
  loading: false
1650
1704
  });
package/lib/embedding.js CHANGED
@@ -186,7 +186,12 @@ const IMPLICIT_EMOTION_PROTOTYPES = Object.freeze([
186
186
  { emotion: "不舍", text: "她站在门口,忍不住回头望了又望,脚却迈不出去。" },
187
187
  { emotion: "无奈", text: "她苦笑了一下,摇了摇头,什么也没说,转身默默收拾东西。" },
188
188
  { emotion: "脆弱", text: "烛火摇曳,她抱住自己蜷缩在角落,轻声啜泣,怕被人听见。" },
189
- { emotion: "苦涩", text: "他把酒喝完,杯子放回桌上时,笑声还在,眼里却没有光了。" }
189
+ { emotion: "苦涩", text: "他把酒喝完,杯子放回桌上时,笑声还在,眼里却没有光了。" },
190
+ // v3.5.0 M7b:4 轴独立原型标签(不占现有情感标签)
191
+ { emotion: "甜宠", text: "他把她圈在怀里,低声哄着,她笑着躲了躲,心里甜得发软。" },
192
+ { emotion: "悬疑", text: "走廊尽头的门虚掩着,地板上有一串陌生的脚印,她屏住呼吸凑近。" },
193
+ { emotion: "唯美", text: "暮色把远山染成淡紫,水面浮着细碎的光,她沿着长堤慢慢走,风很轻。" },
194
+ { emotion: "情欲", text: "灯光昏黄,他俯身靠近,指尖轻轻划过她的锁骨,呼吸渐渐滚烫。" }
190
195
  ]);
191
196
 
192
197
  /**
@@ -359,6 +364,8 @@ function extractChapterTitle(line) {
359
364
  /^Chapter\s+\d+/i.test(stripped) ||
360
365
  /^(序章|序言|楔子|引子|尾声|终章|番外)/.test(stripped)
361
366
  ) {
367
+ // v3.7.0 ⑪:以句号/问号结尾的正文句("第二章内容已更新。")不当标题
368
+ if (/[。!?!?;;,,]$/.test(stripped)) return null;
362
369
  return stripped.slice(0, 30);
363
370
  }
364
371
  return null;