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 +1 -1
- package/lib/analysis.js +81 -33
- package/lib/client.js +83 -29
- package/lib/embedding.js +8 -1
- package/lib/index.js +171 -60
- package/lib/prompts.js +2 -4
- package/lib/style-metrics.js +32 -16
- package/lib/update-check.js +1 -1
- package/lib/vibe.js +12 -6
- package/package.json +2 -2
- package/skills/novel-writing/SKILL.md +3 -3
package/cordis.patch.yml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# dsh-novel-writer v3.
|
|
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
|
-
|
|
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
|
-
|
|
373
|
-
|
|
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, "
|
|
448
|
-
"
|
|
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, "
|
|
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, "
|
|
456
|
-
"厌恶": -0.7, "憎恶": -0.9, "不满": -0.5, "
|
|
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, "
|
|
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
|
-
"
|
|
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,
|
|
469
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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", 恶: "
|
|
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
|
-
|
|
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": "
|
|
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
|
-
|
|
646
|
-
|
|
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") + "
|
|
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") + "
|
|
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 () {
|
|
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
|
-
//
|
|
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: "
|
|
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: "
|
|
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) : ("
|
|
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" } }, "
|
|
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: "
|
|
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 : ("
|
|
1444
|
-
}).catch(function () { props.controller.set({ demoReport: "
|
|
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
|
-
|
|
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;
|