dsh-novel-writer 3.5.0 → 3.6.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/lib/analysis.js +43 -22
- package/lib/client.js +65 -25
- package/lib/embedding.js +6 -1
- package/lib/index.js +65 -30
- package/lib/prompts.js +2 -4
- package/lib/style-metrics.js +18 -13
- package/lib/vibe.js +10 -4
- package/package.json +1 -1
- package/skills/novel-writing/SKILL.md +1 -1
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 复核。 */
|
|
@@ -444,22 +445,20 @@ export function buildGuidance(ratios, topPatterns, chapterSequences) {
|
|
|
444
445
|
*/
|
|
445
446
|
const VALENCE_WORDS = Object.freeze({
|
|
446
447
|
"欣喜": 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,
|
|
448
|
+
"喜悦": 0.7, "愉快": 0.5, "欢喜": 0.7, "兴奋": 0.7, "愉悦": 0.5, "欢快": 0.7, "微笑": 0.3, "笑容": 0.3, "笑眯眯": 0.3, "哈哈": 0.3,
|
|
449
|
+
"欣慰": 0.5, "满足": 0.3, "痛快": 0.5, "爽快": 0.3, "甜蜜": 0.5, "幸福": 0.9,
|
|
450
450
|
"美满": 0.9, "温馨": 0.5, "温暖": 0.3, "踏实": 0.1, "平静": 0.1, "释然": 0.4, "解脱": 0.4,
|
|
451
|
-
"喜欢": 0.7, "喜爱": 0.7, "欣赏": 0.5, "
|
|
451
|
+
"喜欢": 0.7, "喜爱": 0.7, "欣赏": 0.5, "疼爱": 0.7, "宠爱": 0.7, "仰慕": 0.7,
|
|
452
452
|
"尊敬": 0.5, "敬仰": 0.7, "崇拜": 0.7, "心动": 0.5, "眷恋": 0.7, "依恋": 0.7, "思念": 0.3,
|
|
453
453
|
"心疼": 0.3, "怜爱": 0.5, "温柔": 0.3, "珍惜": 0.5, "信赖": 0.5, "感恩": 0.7,
|
|
454
454
|
"愤怒": -0.9, "暴怒": -0.9, "怒发冲冠": -0.9, "火冒三丈": -0.9, "恼羞成怒": -0.9,
|
|
455
|
-
"生气": -0.7, "恼火": -0.7, "气愤": -0.7, "
|
|
456
|
-
"厌恶": -0.7, "憎恶": -0.9, "不满": -0.5, "
|
|
455
|
+
"生气": -0.7, "恼火": -0.7, "气愤": -0.7, "怨恨": -0.9, "憎恨": -0.9,
|
|
456
|
+
"厌恶": -0.7, "憎恶": -0.9, "不满": -0.5, "发火": -0.7, "咬牙": -0.3,
|
|
457
457
|
"怒意": -0.7, "怒火": -0.9, "愤恨": -0.9, "恼羞": -0.7, "气冲冲": -0.7, "咬牙切齿": -0.5,
|
|
458
458
|
"悲伤": -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,
|
|
459
|
+
"伤心": -0.7, "痛苦": -0.7, "心碎": -0.9, "绝望": -0.9, "哭泣": -0.7, "眼泪": -0.3, "流泪": -0.5, "哽咽": -0.5, "抽泣": -0.5, "叹息": -0.3, "叹气": -0.3,
|
|
461
460
|
"惆怅": -0.5, "失落": -0.5, "忧伤": -0.5, "黯然": -0.5, "心酸": -0.7, "辛酸": -0.7,
|
|
462
|
-
"
|
|
461
|
+
"凄凉": -0.7, "苦涩": -0.7, "苦闷": -0.5, "沮丧": -0.7, "消沉": -0.7,
|
|
463
462
|
"落寞": -0.5, "孤寂": -0.5, "郁闷": -0.3, "低落": -0.3, "压抑": -0.5, "心灰意冷": -0.9,
|
|
464
463
|
"恐惧": -0.9, "害怕": -0.7, "惊慌": -0.7, "不安": -0.3, "紧张": -0.3, "担心": -0.3,
|
|
465
464
|
"畏惧": -0.7, "惊恐": -0.9, "胆怯": -0.5, "发抖": -0.3, "哆嗦": -0.3, "心慌": -0.5,
|
|
@@ -603,9 +602,11 @@ export function implicitEmotionScan(text, semResolver = null) {
|
|
|
603
602
|
else { posHits += n * valence; if (false) fragileHits += n; }
|
|
604
603
|
carrierCounts.set(label + ":" + word, (carrierCounts.get(label + ":" + word) ?? 0) + n);
|
|
605
604
|
};
|
|
606
|
-
// ①
|
|
605
|
+
// ① 单方向表(原有)——v3.5.0 M2:跳过与多方向表重叠的词(雨/黄昏/烛火等),防同一出现计 2-3 次(语境裁决交给 ②)
|
|
606
|
+
const ambWords = new Set(Object.keys(AMBIGUOUS_CARRIERS));
|
|
607
607
|
for (const carrier of IMPLICIT_CARRIERS) {
|
|
608
608
|
for (const word of carrier.words) {
|
|
609
|
+
if (ambWords.has(word)) continue;
|
|
609
610
|
const n = text.split(word).length - 1;
|
|
610
611
|
if (n > 0) {
|
|
611
612
|
see(carrier.label, word, n, carrier.valence);
|
|
@@ -698,7 +699,8 @@ export function valenceStats(text) {
|
|
|
698
699
|
const n = series.length;
|
|
699
700
|
if (n === 0) return { windows: 0 };
|
|
700
701
|
const mean = series.reduce((x, y) => x + y, 0) / n;
|
|
701
|
-
|
|
702
|
+
// v3.6.0:样本方差口径 /(n-1)(n=1 时无方差=0)
|
|
703
|
+
const variance = n > 1 ? series.reduce((s, x) => s + (x - mean) ** 2, 0) / (n - 1) : 0;
|
|
702
704
|
let adjSum = 0;
|
|
703
705
|
for (let i = 1; i < n; i += 1) adjSum += Math.abs(series[i] - series[i - 1]);
|
|
704
706
|
const adjVariance = n > 1 ? adjSum / (n - 1) : 0;
|
|
@@ -867,6 +869,8 @@ export function emotionalQuantification(text, perChapter, blocks, semResolver =
|
|
|
867
869
|
* @returns 结构化分析结果(与 novel_sentence_analysis 输出 schema 一致)。
|
|
868
870
|
*/
|
|
869
871
|
export function analyzeText(text, options = {}) {
|
|
872
|
+
// v3.5.0 M6:入口容错(null/undefined 不崩溃)
|
|
873
|
+
text = String(text ?? "");
|
|
870
874
|
const top = Number.isInteger(options.top) ? options.top : 8;
|
|
871
875
|
// v3.5.0 #63:maxSentences 钳制 ≥1(0/负数会让 slice(0,-5) 产生错误语义)
|
|
872
876
|
const maxSentences = Math.max(1, Number.isInteger(options.maxSentences) ? options.maxSentences : 20000);
|
|
@@ -1110,7 +1114,21 @@ export function analyzeText(text, options = {}) {
|
|
|
1110
1114
|
// 主观性指数(启发式 0-100)
|
|
1111
1115
|
const psychRatio = totalSentences === 0 ? 0 : counts.psychology / totalSentences;
|
|
1112
1116
|
const exclaimRatio = totalSentences === 0 ? 0 : counts.exclamation / totalSentences;
|
|
1113
|
-
|
|
1117
|
+
// v3.6.0:第一人称最长匹配优先("我们"不拆成 我+我们 双计)
|
|
1118
|
+
const firstPersonCount = (function () {
|
|
1119
|
+
let fpSum = 0;
|
|
1120
|
+
const fpSorted = FIRST_PERSON_WORDS.slice().sort((x, y) => y.length - x.length);
|
|
1121
|
+
let fpText = text;
|
|
1122
|
+
for (const w of fpSorted) {
|
|
1123
|
+
if (w.length < 2) continue;
|
|
1124
|
+
const parts = fpText.split(w);
|
|
1125
|
+
fpSum += parts.length - 1;
|
|
1126
|
+
fpText = parts.join("\u0000".repeat(w.length));
|
|
1127
|
+
}
|
|
1128
|
+
// 剩余孤立单字"我"
|
|
1129
|
+
fpSum += (fpText.match(/我/g) || []).length;
|
|
1130
|
+
return fpSum;
|
|
1131
|
+
})();
|
|
1114
1132
|
const firstPersonDensity = totalChars === 0 ? 0 : (firstPersonCount / totalChars) * 1000;
|
|
1115
1133
|
const subjectivityIndex = Math.min(100, Math.round(
|
|
1116
1134
|
psychRatio * 50 + exclaimRatio * 60 + Math.min(emotionDensity * 2.5, 25) + Math.min(firstPersonDensity * 1.2, 20)
|
|
@@ -1204,6 +1222,8 @@ function emotionCurve(blockMeta, maxSegments) {
|
|
|
1204
1222
|
intensity: chars === 0 ? 0 : round((total / chars) * 1000, 2)
|
|
1205
1223
|
});
|
|
1206
1224
|
}
|
|
1225
|
+
// v3.6.0 复审修正:不裁剪——强度 0 段是"平缓段"(跨章节曲线对齐需要固定段数契约);
|
|
1226
|
+
// segmentCount=min(blocks, maxSegments) 已保证每段至少 1 块,无空 slice
|
|
1207
1227
|
return curve;
|
|
1208
1228
|
}
|
|
1209
1229
|
|
|
@@ -1330,7 +1350,7 @@ function loadDutir() {
|
|
|
1330
1350
|
}
|
|
1331
1351
|
return DUTIR;
|
|
1332
1352
|
}
|
|
1333
|
-
const DUTIR_TO_EMOTION = { 乐: "joy", 好: "joy", 怒: "anger", 哀: "sorrow", 惧: "fear", 恶: "
|
|
1353
|
+
const DUTIR_TO_EMOTION = { 乐: "joy", 好: "joy", 怒: "anger", 哀: "sorrow", 惧: "fear", 恶: "anger", 惊: "surprise" }; // v3.5.0 H4:恶(厌恶) 归 anger 而非 sorrow
|
|
1334
1354
|
const dutirLookup = new Map();
|
|
1335
1355
|
function dutirEmotion(word) {
|
|
1336
1356
|
if (dutirLookup.size === 0) {
|
|
@@ -1338,7 +1358,8 @@ function dutirEmotion(word) {
|
|
|
1338
1358
|
for (const [cat, words] of Object.entries(dutir)) {
|
|
1339
1359
|
const emo = DUTIR_TO_EMOTION[cat];
|
|
1340
1360
|
if (!emo) continue;
|
|
1341
|
-
|
|
1361
|
+
// v3.5.0 H4:跨类冲突词首见保留(后写不再覆盖——"开心"不会被 恶 类覆盖成 sorrow)
|
|
1362
|
+
for (const w of words) if (typeof w === "string" && w.length >= 2 && !dutirLookup.has(w)) dutirLookup.set(w, emo);
|
|
1342
1363
|
}
|
|
1343
1364
|
}
|
|
1344
1365
|
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",
|
|
@@ -667,7 +703,7 @@ var css = `
|
|
|
667
703
|
});
|
|
668
704
|
return remote;
|
|
669
705
|
}).catch(function () {
|
|
670
|
-
api.set({ hostOk: false, loading: false, refreshing: false, revealMsg: t("plot.revealErr") + "
|
|
706
|
+
api.set({ hostOk: false, loading: false, refreshing: false, revealMsg: t("plot.revealErr") + ":" + t("state.unreachable"), revealErr: true });
|
|
671
707
|
return null;
|
|
672
708
|
});
|
|
673
709
|
},
|
|
@@ -687,7 +723,7 @@ var css = `
|
|
|
687
723
|
api.set({ revealMsg: data.error || t("plot.revealErr"), revealErr: true });
|
|
688
724
|
}
|
|
689
725
|
}).catch(function () {
|
|
690
|
-
api.set({ revealMsg: t("plot.revealErr") + "
|
|
726
|
+
api.set({ revealMsg: t("plot.revealErr") + ":" + t("state.unreachable"), revealErr: true });
|
|
691
727
|
}).finally(function () {
|
|
692
728
|
api.set({ revealing: false });
|
|
693
729
|
});
|
|
@@ -1056,8 +1092,8 @@ var TOOL_GROUPS = [
|
|
|
1056
1092
|
// v3.0.0:风格基线——六维文笔指标 ±% 容差带(左=允许低于,右=允许高于)
|
|
1057
1093
|
var DEFAULT_TOL = { low: "", high: "" };
|
|
1058
1094
|
var NW_METRICS = [
|
|
1059
|
-
["complexity", "句法复杂度", "📐"], ["modifierDensity", "修饰密度", "🎨"], ["abstractDensity", "抽象度", "☁️"],
|
|
1060
|
-
["actionDensity", "动作密度", "⚡"], ["hedgeDensity", "不确定性", "🌫️"], ["gapIndex", "留白指数", "🕳️"]
|
|
1095
|
+
["complexity", "句法复杂度", "📐", "Syntax"], ["modifierDensity", "修饰密度", "🎨", "Modifier"], ["abstractDensity", "抽象度", "☁️", "Abstract"],
|
|
1096
|
+
["actionDensity", "动作密度", "⚡", "Action"], ["hedgeDensity", "不确定性", "🌫️", "Hedge"], ["gapIndex", "留白指数", "🕳️", "Gap"]
|
|
1061
1097
|
];
|
|
1062
1098
|
// v3.0.0:输入框留空 = 使用推荐容差(原著章节波动 1.5σ);填了才保存自定义
|
|
1063
1099
|
var tolState = state.styleTolerance && typeof state.styleTolerance === "object" ? state.styleTolerance : null;
|
|
@@ -1091,13 +1127,19 @@ var TOOL_GROUPS = [
|
|
|
1091
1127
|
setTimeout(function () { props.controller.set({ baselineSaved: false }); }, 1800);
|
|
1092
1128
|
};
|
|
1093
1129
|
var saveTol = function () {
|
|
1094
|
-
//
|
|
1130
|
+
// v3.5.0 M18:单侧填写提示成对(不再静默丢弃)
|
|
1131
|
+
var halfFilled = null;
|
|
1095
1132
|
var tol = {};
|
|
1096
1133
|
var any = false;
|
|
1097
1134
|
for (var si = 0; si < NW_METRICS.length; si += 1) {
|
|
1098
1135
|
var sk = NW_METRICS[si][0];
|
|
1099
1136
|
var sd = draft[sk];
|
|
1100
1137
|
if (sd && sd.low !== "" && sd.high !== "") { tol[sk] = { low: -sd.low, high: sd.high }; any = true; }
|
|
1138
|
+
else if (sd && (sd.low !== "" || sd.high !== "")) { halfFilled = halfFilled || sk; }
|
|
1139
|
+
}
|
|
1140
|
+
if (halfFilled) {
|
|
1141
|
+
props.controller.set({ revealMsg: t("tol.halfFilled") + "「" + halfFilled + "」" + t("tol.halfFilledTail"), revealErr: true });
|
|
1142
|
+
return;
|
|
1101
1143
|
}
|
|
1102
1144
|
props.toggle({ styleTolerance: any ? tol : null });
|
|
1103
1145
|
props.controller.set({ baselineDraft: null, revealMsg: t("baseline.saved"), revealErr: false });
|
|
@@ -1119,11 +1161,11 @@ var TOOL_GROUPS = [
|
|
|
1119
1161
|
var mk2 = item[0];
|
|
1120
1162
|
var d = draft[mk2] || DEFAULT_TOL;
|
|
1121
1163
|
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]),
|
|
1164
|
+
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
1165
|
el("div", { className: "nwTolField", style: { width: "74px", flexShrink: "0", display: "flex", flexDirection: "column", gap: "3px" } },
|
|
1124
1166
|
el("div", { style: { display: "flex", alignItems: "center", gap: "4px" } },
|
|
1125
1167
|
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: "
|
|
1168
|
+
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
1169
|
),
|
|
1128
1170
|
el("span", { className: "nwTolCaption", style: { fontSize: "10px", color: "#94a3b8", textAlign: "center" } }, t("baseline.low"))
|
|
1129
1171
|
),
|
|
@@ -1131,7 +1173,7 @@ var TOOL_GROUPS = [
|
|
|
1131
1173
|
el("div", { className: "nwTolField", style: { width: "74px", flexShrink: "0", display: "flex", flexDirection: "column", gap: "3px" } },
|
|
1132
1174
|
el("div", { style: { display: "flex", alignItems: "center", gap: "4px" } },
|
|
1133
1175
|
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: "
|
|
1176
|
+
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
1177
|
),
|
|
1136
1178
|
el("span", { className: "nwTolCaption", style: { fontSize: "10px", color: "#94a3b8", textAlign: "center" } }, t("baseline.high"))
|
|
1137
1179
|
),
|
|
@@ -1146,14 +1188,13 @@ var TOOL_GROUPS = [
|
|
|
1146
1188
|
);
|
|
1147
1189
|
} else if (view === "reports") {
|
|
1148
1190
|
// v3.2.0:报告历史(加载在入口 onClick 触发,此处纯渲染防 React 345 循环)
|
|
1149
|
-
var reportReqId = 0;
|
|
1150
1191
|
var openReport = function (file) {
|
|
1151
1192
|
var reqId = ++reportReqId;
|
|
1152
|
-
props.controller.set({ reportContent: "
|
|
1193
|
+
props.controller.set({ reportContent: t("reports.loading") });
|
|
1153
1194
|
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
1195
|
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: "
|
|
1196
|
+
props.controller.set({ reportContent: d.ok ? JSON.stringify(d.content, null, 2) : (t("reports.readFail") + ":" + (d.error || "")) });
|
|
1197
|
+
}).catch(function () { props.controller.set({ reportContent: t("reports.readFailDetail") }); });
|
|
1157
1198
|
};
|
|
1158
1199
|
var groups = state.reportsGroups || [];
|
|
1159
1200
|
var totalFiles = groups.reduce(function (a, g) { return a + (g.files || []).length; }, 0);
|
|
@@ -1168,7 +1209,7 @@ var TOOL_GROUPS = [
|
|
|
1168
1209
|
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
1210
|
)
|
|
1170
1211
|
: el("div", { style: { display: "flex", flexDirection: "column", gap: "8px" } },
|
|
1171
|
-
reportsLoading ? el("div", { style: { fontSize: "12px", color: "#94a3b8", padding: "10px 0" } }, "
|
|
1212
|
+
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
1213
|
groups.map(function (g) {
|
|
1173
1214
|
return el("div", { key: g.name },
|
|
1174
1215
|
el("div", { style: { fontSize: "12px", fontWeight: "600", color: "#475569", margin: "6px 0 4px" } }, g.name + "(" + (g.files || []).length + ")"),
|
|
@@ -1185,12 +1226,12 @@ var TOOL_GROUPS = [
|
|
|
1185
1226
|
} else if (view === "creation" || view === "creation-form") {
|
|
1186
1227
|
// v3.1.0:原创模式设定库(方案 C:列表页 + 表单页 + 快速切换)
|
|
1187
1228
|
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"), "
|
|
1229
|
+
["worldview", "🌍", t("creation.worldview"), t("ph.e末")],
|
|
1230
|
+
["characters", "🎭", t("creation.characters"), t("ph.e女")],
|
|
1231
|
+
["forbidden", "🚫", t("creation.forbidden"), t("ph.e不")],
|
|
1232
|
+
["mainConflict", "🎯", t("creation.mainConflict"), t("ph.e主")],
|
|
1233
|
+
["genre", "📚", t("creation.genre"), t("ph.e悬")],
|
|
1234
|
+
["extra", "📝", t("creation.extra"), t("ph.e每")]
|
|
1194
1235
|
];
|
|
1195
1236
|
var cpGlobal = state.creationProfile && typeof state.creationProfile === "object" ? state.creationProfile : {};
|
|
1196
1237
|
var cpBooks = state.creationProfiles && typeof state.creationProfiles === "object" ? state.creationProfiles : {};
|
|
@@ -1230,7 +1271,7 @@ var TOOL_GROUPS = [
|
|
|
1230
1271
|
var nb = String(newBookName || "").trim();
|
|
1231
1272
|
// v3.5.0 #49:客户端校验——路径分隔符/Windows 保留字符直接提示
|
|
1232
1273
|
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: "
|
|
1274
|
+
props.controller.set({ revealMsg: t("creation.badName"), revealErr: true });
|
|
1234
1275
|
return;
|
|
1235
1276
|
}
|
|
1236
1277
|
if (!nb) {
|
|
@@ -1402,7 +1443,6 @@ var TOOL_GROUPS = [
|
|
|
1402
1443
|
),
|
|
1403
1444
|
pathBox(t("dir.data"), dirs.dataDir || state.dataDir || "", "data-dir"),
|
|
1404
1445
|
// v3.2.0:书库统计(写作打卡);null=未就绪不渲染,[]=真空提示
|
|
1405
|
-
state.booksStats === null ? null :
|
|
1406
1446
|
el("div", { key: "statsCard", style: { marginTop: "10px", padding: "10px 12px", borderRadius: "10px", border: "1px solid #e2e8f0", background: "#fff" } },
|
|
1407
1447
|
el("div", { style: { fontSize: "12px", fontWeight: "600", color: "#475569", marginBottom: "6px" } }, "📚 " + t("stats.title")),
|
|
1408
1448
|
el("div", { style: { display: "flex", flexDirection: "column", gap: "5px" } },
|
|
@@ -1440,8 +1480,8 @@ var TOOL_GROUPS = [
|
|
|
1440
1480
|
if (state.demoLoading) return;
|
|
1441
1481
|
props.controller.set({ demoLoading: true });
|
|
1442
1482
|
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: "
|
|
1483
|
+
props.controller.set({ demoReport: d.ok ? d.report : (t("demo.fail") + ":" + (d.error || "")), demoLoading: false });
|
|
1484
|
+
}).catch(function () { props.controller.set({ demoReport: t("reports.readFailDetail"), demoLoading: false }); });
|
|
1445
1485
|
} },
|
|
1446
1486
|
el("span", { style: { fontSize: "13px", fontWeight: "600", color: "#6366f1" } }, state.demoLoading ? t("demo.loading") : t("demo.run")),
|
|
1447
1487
|
el("span", { style: { fontSize: "11px", color: "#94a3b8" } }, t("demo.hint"))
|
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
|
/**
|
package/lib/index.js
CHANGED
|
@@ -396,11 +396,16 @@ function bookNameFromContent(text) {
|
|
|
396
396
|
const line = raw.replace(/^#+\s*/, "").replace(/^\uFEFF/, "").trim();
|
|
397
397
|
if (line === "") continue;
|
|
398
398
|
if (IMPORT_NOISE.has(line)) continue;
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
399
|
+
// v3.5.0 M9b:《书名》提取在前(《斗破苍穹》第一章 陨落 同行也能识别书名)——先看有没有《》包裹
|
|
400
|
+
const bookMatch = line.match(/[《〈]\s*([^》〉]{2,30}?)\s*[》〉]/);
|
|
401
|
+
if (bookMatch) {
|
|
402
|
+
const name = bookMatch[1].trim();
|
|
403
|
+
if (name.length >= 2 && name.length <= 30) return name;
|
|
404
|
+
continue;
|
|
403
405
|
}
|
|
406
|
+
// v3.5.0 M9b:章号行不是书名——跳过(防止章节标题当书名;第一章 初遇 无《》落这里)
|
|
407
|
+
const hasChapterMark = /第?(\d{1,4})[章回话]|第[零一二三四五六七八九十百两]+[章回话]|^\d{1,4}[-._\s]/.test(line);
|
|
408
|
+
if (hasChapterMark) continue;
|
|
404
409
|
}
|
|
405
410
|
return void 0;
|
|
406
411
|
}
|
|
@@ -673,9 +678,24 @@ const TOOL_LABELS = Object.freeze({
|
|
|
673
678
|
function detectCulture(text) {
|
|
674
679
|
const scores = { western: 0, eastern: 0, modern: 0 };
|
|
675
680
|
const evidence = { western: [], eastern: [], modern: [] };
|
|
681
|
+
// v3.5.0 M4:词表去重扫描(同表重复词如 城堡×2/便利店×2 不再双计)
|
|
682
|
+
// v3.6.0:同文化子串最长匹配("高潮迭起"命中后 "高潮" 不再重复计)
|
|
676
683
|
for (const [culture, table] of Object.entries(CULTURE_MARKERS)) {
|
|
677
|
-
|
|
678
|
-
|
|
684
|
+
const seenWords = new Set();
|
|
685
|
+
const sorted = table.words.slice().sort(function (a, b) { return b.length - a.length; });
|
|
686
|
+
let scanText = text;
|
|
687
|
+
for (const word of sorted) {
|
|
688
|
+
if (seenWords.has(word)) continue;
|
|
689
|
+
seenWords.add(word);
|
|
690
|
+
let n = 0;
|
|
691
|
+
let from = 0;
|
|
692
|
+
while (true) {
|
|
693
|
+
const hit = scanText.indexOf(word, from);
|
|
694
|
+
if (hit === -1) break;
|
|
695
|
+
n += 1;
|
|
696
|
+
scanText = scanText.slice(0, hit) + "\u0000".repeat(word.length) + scanText.slice(hit + word.length);
|
|
697
|
+
from = hit + word.length;
|
|
698
|
+
}
|
|
679
699
|
if (n > 0) {
|
|
680
700
|
scores[culture] += n;
|
|
681
701
|
if (evidence[culture].length < 8) evidence[culture].push(word + "×" + n);
|
|
@@ -732,8 +752,19 @@ function detectTheme(text) {
|
|
|
732
752
|
for (const [theme, words] of Object.entries(THEME_MARKERS)) {
|
|
733
753
|
let count = 0;
|
|
734
754
|
const found = [];
|
|
735
|
-
|
|
736
|
-
|
|
755
|
+
// v3.5.0:同主题子串最长匹配("高潮迭起"命中后 "高潮" 不再重复计;先长后短 + 占位)
|
|
756
|
+
const sorted = words.slice().sort(function (x, y) { return y.length - x.length; });
|
|
757
|
+
let scanText = text;
|
|
758
|
+
for (const word of sorted) {
|
|
759
|
+
let n = 0;
|
|
760
|
+
let from = 0;
|
|
761
|
+
while (true) {
|
|
762
|
+
const hit = scanText.indexOf(word, from);
|
|
763
|
+
if (hit === -1) break;
|
|
764
|
+
n += 1;
|
|
765
|
+
scanText = scanText.slice(0, hit) + "\u0000".repeat(word.length) + scanText.slice(hit + word.length);
|
|
766
|
+
from = hit + word.length;
|
|
767
|
+
}
|
|
737
768
|
if (n > 0) { count += n; if (found.length < 6) found.push(word + "×" + n); }
|
|
738
769
|
}
|
|
739
770
|
if (count > 0) { hits[theme] = count; evidence[theme] = found; }
|
|
@@ -1205,6 +1236,8 @@ function makeStateRoutes(allowLan, config) {
|
|
|
1205
1236
|
// v2.6.5:更新检查路由(GitHub Releases API + 24h 缓存 + 3s 超时,全程静默)
|
|
1206
1237
|
const updateCheckHandler = async (req, res) => {
|
|
1207
1238
|
if (!isAllowedRequest(req, allowLan)) return writeJson(res, 403, { error: "forbidden: loopback-only" });
|
|
1239
|
+
// v3.5.0 L3:仅 GET(POST 也会触发 GitHub 请求)
|
|
1240
|
+
if ((req.method ?? "GET") !== "GET") return writeJson(res, 405, { error: "method not allowed" });
|
|
1208
1241
|
const result = await checkForUpdate(PLUGIN_VERSION, join(homedir(), ".dsh", "dsh-novel-writer"));
|
|
1209
1242
|
return writeJson(res, 200, result);
|
|
1210
1243
|
};
|
|
@@ -1535,7 +1568,9 @@ function registerNovelSentenceAnalysis(ctx, config) {
|
|
|
1535
1568
|
const top3b = cached.categories.slice().sort(function (a, b) { return b.count - a.count; }).slice(0, 3).map(function (c) { return c.label + " " + Math.round(c.ratio * 100) + "%"; });
|
|
1536
1569
|
cached.brief = "全书 " + cached.totalSentences + " 句:句式以 " + top3b.join("、") + " 为主;均句长 " + cached.lengths.avg.toFixed(1) + ";主导情绪 " + (cached.emotion.dominant || "-") + "。";
|
|
1537
1570
|
}
|
|
1538
|
-
|
|
1571
|
+
// v3.5.0 H2:缓存命中剥离内部 ver 键(契约:output schema 无 ver)
|
|
1572
|
+
const { ver: _ver, ...rest } = cached;
|
|
1573
|
+
return { ...rest, book, scope, cache: "hit", reportFile, cachedAt: cached.cachedAt ?? new Date().toISOString() };
|
|
1539
1574
|
} catch { /* 缓存缺失/损坏,重新分析 */ }
|
|
1540
1575
|
}
|
|
1541
1576
|
const options = { top, maxSentences, curveSegments };
|
|
@@ -1650,6 +1685,8 @@ ${featLines.join("\n")}
|
|
|
1650
1685
|
for (const name of ALL_TOOLS) {
|
|
1651
1686
|
if (typeof args.tools[name] === "boolean") toolsPatch[name] = args.tools[name];
|
|
1652
1687
|
}
|
|
1688
|
+
// v3.5.0 M13:novel_sentence_config 禁止被关(关了自己就无法再开——死锁)
|
|
1689
|
+
delete toolsPatch.novel_sentence_config;
|
|
1653
1690
|
if (Object.keys(toolsPatch).length > 0) patch.tools = toolsPatch;
|
|
1654
1691
|
}
|
|
1655
1692
|
if (args?.features !== null && typeof args?.features === "object") {
|
|
@@ -2048,7 +2085,9 @@ async function bookStats(root) {
|
|
|
2048
2085
|
if (!/\.(md|txt)$/i.test(f.name)) continue;
|
|
2049
2086
|
const st = await stat(join(bookPath, f.name));
|
|
2050
2087
|
chapters += 1;
|
|
2051
|
-
|
|
2088
|
+
// v3.5.0 M11:与其余工具同口径解码(GBK/UTF-16 章节字数不再按 UTF-8 误读)
|
|
2089
|
+
const buf11 = await readFile(join(bookPath, f.name));
|
|
2090
|
+
const txt = decodeTextBuffer(buf11, join(bookPath, f.name));
|
|
2052
2091
|
chars += txt.length;
|
|
2053
2092
|
if (st.mtimeMs >= weekAgo) recent7 += txt.length;
|
|
2054
2093
|
}
|
|
@@ -2144,18 +2183,7 @@ function buildCreationCharacterSection(book) {
|
|
|
2144
2183
|
return "";
|
|
2145
2184
|
}
|
|
2146
2185
|
|
|
2147
|
-
|
|
2148
|
-
async function readCreationStatus(root, book, exec) {
|
|
2149
|
-
const dir = creationDir(root, book);
|
|
2150
|
-
const f = join(dir, CREATION_FILES.status);
|
|
2151
|
-
try {
|
|
2152
|
-
const txt = await readTextFile(f, exec);
|
|
2153
|
-
// 检测未回填钩子(钩子记录有章节号但状态卡进度低于它?——直接比对大纲与钩子)
|
|
2154
|
-
return txt;
|
|
2155
|
-
} catch {
|
|
2156
|
-
return null;
|
|
2157
|
-
}
|
|
2158
|
-
}
|
|
2186
|
+
|
|
2159
2187
|
|
|
2160
2188
|
function createOutlineTool(config) {
|
|
2161
2189
|
return {
|
|
@@ -2444,7 +2472,7 @@ function plotKeywords(content) {
|
|
|
2444
2472
|
function normalizePlotEntry(entry) {
|
|
2445
2473
|
// v3.5.0 #17:脏数据兜底——非对象或缺 id/content 直接跳过(不污染输出)
|
|
2446
2474
|
if (!entry || typeof entry !== "object" || typeof entry.id !== "string" || typeof entry.content !== "string") return null;
|
|
2447
|
-
const out = { id: entry.id, content: entry.content, status:
|
|
2475
|
+
const out = { id: entry.id, content: entry.content, status: entry.status === "done" ? "done" : "open" }; // v3.5.0 M12:status 归一(脏数据 in-progress 等一律 open,符合 schema enum)
|
|
2448
2476
|
for (const key of ["chapter", "note", "payoffCondition", "lastMentioned", "type", "priority"]) {
|
|
2449
2477
|
if (entry[key] !== void 0 && entry[key] !== null) out[key] = String(entry[key]);
|
|
2450
2478
|
}
|
|
@@ -2525,6 +2553,7 @@ function registerNovelStyleReport(ctx, config) {
|
|
|
2525
2553
|
book: { type: "string", description: "书名(novels 下的子目录名)" },
|
|
2526
2554
|
root: { type: "string", description: "章节库根目录(含 novels 子目录)。" },
|
|
2527
2555
|
action: { type: "string", description: "report=生成测量报告(默认);get=读取已保存的 AI 风格判断" },
|
|
2556
|
+
brief: { type: "boolean", description: "true=返回精简摘要(一句话结论),省略=完整报告。" },
|
|
2528
2557
|
aiJudgment: { type: "string", description: "AI 的风格气质判断结论:后插件将测量数据与判断存入 style-reports 供后续使用" }
|
|
2529
2558
|
},
|
|
2530
2559
|
required: ["book"]
|
|
@@ -2904,7 +2933,8 @@ function registerNovelSettings(ctx, config) {
|
|
|
2904
2933
|
} else if (action === "update" || action === "delete") {
|
|
2905
2934
|
const name = optionalString(args, "name") ?? optionalString(args, "day");
|
|
2906
2935
|
assert(name !== void 0, `novel_settings ${action} 需要 name(timeline 用 day)参数`);
|
|
2907
|
-
|
|
2936
|
+
// v3.5.0 M10:timeline 条目 name=day 值——update 按新 day 传时双查(name 或 day 字段)
|
|
2937
|
+
const index = list.findIndex((e) => e.name === name || (category === "timeline" && e.day === name));
|
|
2908
2938
|
assert(index !== -1, `${categoryLabel}中不存在「${name}」`);
|
|
2909
2939
|
if (action === "delete") {
|
|
2910
2940
|
list.splice(index, 1);
|
|
@@ -3650,8 +3680,11 @@ function registerNovelPlot(ctx, config) {
|
|
|
3650
3680
|
message = `已更新伏笔 #${id}`;
|
|
3651
3681
|
}
|
|
3652
3682
|
}
|
|
3653
|
-
|
|
3654
|
-
|
|
3683
|
+
// v3.5.0 M8:list(只读)不写盘——读操作不做多余 I/O、只读挂载不失败
|
|
3684
|
+
if (action !== "list") {
|
|
3685
|
+
await writePlots(root, book, entries);
|
|
3686
|
+
try { await writeSentenceState({ lastRoot: root }); } catch { /* 记录失败不影响伏笔功能 */ }
|
|
3687
|
+
}
|
|
3655
3688
|
// v3.5.0 #17:旧/手改数据缺 createdAt 兜底(undefined.localeCompare 会 TypeError)
|
|
3656
3689
|
// v3.5.0 #17:先清洗(过滤脏条目/补默认值)再排序——排序不再碰 null
|
|
3657
3690
|
const cleanEntries = entries.map(normalizePlotEntry).filter(Boolean);
|
|
@@ -4046,7 +4079,7 @@ ${footer}
|
|
|
4046
4079
|
function registerNovelKeywords(ctx, config) {
|
|
4047
4080
|
ctx.tools.register({
|
|
4048
4081
|
name: "novel_keywords",
|
|
4049
|
-
description: "
|
|
4082
|
+
description: "统计某部作品(或单个章节)中出现频率较高的关键词:中文相邻二字/三字词组与英文词(novel_keywords 输出不含单字)。",
|
|
4050
4083
|
parameters: {
|
|
4051
4084
|
type: "object",
|
|
4052
4085
|
properties: {
|
|
@@ -4203,9 +4236,11 @@ Created 第${String(value.number).padStart(2, "0")}章 (${value.chars} chars)
|
|
|
4203
4236
|
}
|
|
4204
4237
|
const fileName = `第${String(number).padStart(2, "0")}章.md`;
|
|
4205
4238
|
const filePath = join(dir, fileName);
|
|
4206
|
-
// v3.5.0 #2
|
|
4207
|
-
|
|
4208
|
-
|
|
4239
|
+
// v3.5.0 #2/M14:防覆盖——章号归一化查重(第1章/第一章/第01章 视为同号,避免同号重复章并存)
|
|
4240
|
+
const existingSame = await scanChapters(dir);
|
|
4241
|
+
const sameNo = existingSame.find(function (c) { return (c.number ?? -1) === number; });
|
|
4242
|
+
if (sameNo) {
|
|
4243
|
+
throw new Error(`第 ${number} 章已存在(${sameNo.file})——不会覆盖旧稿;如需更新请先确认或使用其他章号`);
|
|
4209
4244
|
}
|
|
4210
4245
|
const parts = [];
|
|
4211
4246
|
if (title !== void 0) parts.push(`# ${title}`, "");
|
package/lib/prompts.js
CHANGED
|
@@ -31,10 +31,8 @@ const WORKFLOW_TEXT = [
|
|
|
31
31
|
" 重要:句式模式是「参考节奏」而非「模板套用」。若机械复刻导致句子僵硬、重复、模式化,必须优先回归自然表达。",
|
|
32
32
|
"5. 导入/整理原稿件时:用 novel_import 扫描存放多本小说稿件的文件夹(src),先以 scan 模式查看分组建议,",
|
|
33
33
|
" 若发现异名同书(如\"旧版/精修版\"实为同一本),用 book 参数强制合并后以 apply 模式导入到 novels/<书名>/ 分类存放。",
|
|
34
|
-
"6.
|
|
35
|
-
"
|
|
36
|
-
"7. 每次续写前先简述:上一章结尾状态 → 本章目标 → 写作计划,再给出正文。",
|
|
37
|
-
"8. 工具链提示:续写/分析前可用 novel_plot 查看未回收伏笔(open 条目,含类型/优先级/提及章节);写完新章节可用 novel_style_check 做风格自检(相似度+偏差+细节密度),novel_plot scan 自动更新伏笔提及记录;",
|
|
34
|
+
"6. 每次续写前先简述:上一章结尾状态 → 本章目标 → 写作计划,再给出正文。",
|
|
35
|
+
"7. 工具链提示:续写/分析前可用 novel_plot 查看未回收伏笔(open 条目,含类型/优先级/提及章节);写完新章节可用 novel_style_check 做风格自检(相似度+偏差+细节密度),novel_plot scan 自动更新伏笔提及记录;",
|
|
38
36
|
" 世界观一致性:续写前先确认文化基准——novel_settings category=worldview action=detect 自动判断(或人工 add/update),动笔时对照其 bannedWords/recommended 用词,避免中西意象混搭(如欧式背景不写老夫/上香/时辰);novel_continuity_check 会扫描用语冲突候选;",
|
|
39
37
|
" 语用一致性:不只管词,还要管'怎么说话'——对照 worldview 的 speechStyle(title 称谓规范/honorBad 客套禁词/ritualBadPatterns 仪式禁式/tone 语气),人物开口前检查称谓是否欧式(Miss+名)、客套是否避免'提点/承蒙/在下'、宗教仪式是否点烛而非烧香/上X柱香、对话是否口语化不文言;",
|
|
40
38
|
" novel_settings 维护五张设定表(人物/地点/道具/时间线/世界观用语规范),novel_summary 保存每章摘要(长书续写先读摘要再按需细读),novel_continuity_check 输出设定矛盾候选;",
|
package/lib/style-metrics.js
CHANGED
|
@@ -31,7 +31,7 @@ const ACTION_VERBS = [
|
|
|
31
31
|
"推", "拉", "抓", "拿", "端", "抱", "举", "抬", "踢", "踩", "打", "拍", "敲", "砸",
|
|
32
32
|
"扔", "丢", "接", "递", "握", "捏", "掐", "拧", "扯", "撕", "拔", "插", "捅", "刺",
|
|
33
33
|
"砍", "劈", "切", "割", "放", "摆", "搁", "挂", "贴", "塞", "填", "灌", "倒", "泼",
|
|
34
|
-
"洒", "翻", "卷", "铺", "盖", "叠", "折", "脱", "穿", "戴", "摘",
|
|
34
|
+
"洒", "翻", "卷", "铺", "盖", "叠", "折", "脱", "穿", "戴", "摘", "解", "开",
|
|
35
35
|
"关", "锁", "按", "压", "顶", "撞", "碰", "触", "摸", "抚", "揉", "搓", "擦", "抹",
|
|
36
36
|
"洗", "刷", "扫", "拖", "铲", "挖", "埋", "堆", "砌", "修", "补", "缝", "织", "编",
|
|
37
37
|
"缠", "绕", "捆", "绑", "扎", "拴", "套", "蒙", "罩", "捂", "堵", "挡", "遮", "掩",
|
|
@@ -43,29 +43,29 @@ const ACTION_VERBS = [
|
|
|
43
43
|
"吸", "呼", "吐", "咽", "吞", "嚼", "咬", "啃", "舔", "吮", "嗅", "闻", "听", "看",
|
|
44
44
|
"望", "瞧", "瞅", "盯", "瞪", "瞥", "瞟", "瞄", "观", "察", "读", "写", "画", "描",
|
|
45
45
|
"绘", "刻", "雕", "铸", "炼", "烧", "煮", "炒", "煎", "炸", "烤", "烘", "熏", "泡",
|
|
46
|
-
"浸", "染", "涂", "喷", "浇", "淋", "滴",
|
|
46
|
+
"浸", "染", "涂", "喷", "浇", "淋", "滴", "淌", "涌", "冒", "射", "溅", "炸",
|
|
47
47
|
"裂", "碎", "破", "赢", "输", "借", "还", "付", "收", "给", "送", "寄", "递", "交",
|
|
48
48
|
"赐", "赏", "罚", "奖", "惩", "减", "增", "加", "删", "改", "换", "替", "代", "变",
|
|
49
49
|
"创", "建", "设", "置", "安", "装", "配", "组", "拆", "卸", "运", "搬", "携", "带",
|
|
50
|
-
"佩", "持", "执", "操", "控", "驾", "驶", "骑", "乘", "踏", "迈",
|
|
50
|
+
"佩", "持", "执", "操", "控", "驾", "驶", "骑", "乘", "踏", "迈", "驰", "飞",
|
|
51
51
|
"翔", "游", "泳", "潜", "航", "驱", "逐", "灭", "杀", "斩", "擒", "捕", "捉", "逮",
|
|
52
52
|
"拘", "押", "囚", "禁", "铐", "缚", "吊", "悬", "垂", "坠", "掉", "摔", "跌", "倒",
|
|
53
|
-
"仆", "瘫",
|
|
53
|
+
"仆", "瘫", "麻", "僵", "直", "挺", "竖", "耸", "矗", "凸", "凹", "陷", "塌",
|
|
54
54
|
"崩", "垮", "弯", "折", "断", "裂", "绽", "合", "拢", "闭", "张", "启", "展", "伸",
|
|
55
55
|
"缩", "抽", "拽", "揪", "捋", "拂", "掸", "扑", "扇", "燃", "点", "着", "焚", "灼",
|
|
56
56
|
"烫", "烙", "煨", "炖", "焖", "熬", "烹", "调", "拌", "搅", "和", "团", "发", "烂",
|
|
57
|
-
"腐", "朽", "酸", "甜", "苦", "辣", "咸",
|
|
57
|
+
"腐", "朽", "酸", "甜", "苦", "辣", "咸", "腻", "胖", "肿", "胀", "鼓", "瘪",
|
|
58
58
|
"皱", "平", "整", "齐", "乱", "杂", "混", "浊", "清", "澈", "净", "洁", "污", "染",
|
|
59
|
-
"脏", "臭", "香", "芳", "馥", "郁", "浓",
|
|
59
|
+
"脏", "臭", "香", "芳", "馥", "郁", "浓", "雅", "俗", "野", "蛮", "横", "霸",
|
|
60
60
|
"欺", "压", "榨", "剥", "削", "掠", "夺", "抢", "劫", "偷", "盗", "窃", "扒", "掏",
|
|
61
61
|
"搜", "查", "寻", "找", "觅", "探", "索", "究", "研", "讨", "论", "议", "评", "判",
|
|
62
|
-
"审", "核", "验", "证", "试", "测", "量", "计", "算",
|
|
62
|
+
"审", "核", "验", "证", "试", "测", "量", "计", "算", "分", "析", "归", "纳",
|
|
63
63
|
"辩", "驳", "争", "吵", "闹", "斗", "殴", "拼", "搏", "挣", "抗", "抵", "防", "守",
|
|
64
64
|
"卫", "护", "保", "救", "援", "助", "帮", "扶", "领", "导", "指", "引", "教", "训",
|
|
65
65
|
"培", "锻", "磨", "砺", "激", "励", "鼓", "振", "奋", "兴", "感", "触", "震", "惊",
|
|
66
66
|
"慨", "唏", "嘘", "呜", "咽", "啜", "泣", "哽", "窒", "憋", "闷", "堵", "慌", "乱",
|
|
67
67
|
"躁", "烦", "恼", "怒", "火", "气", "愤", "憎", "恶", "厌", "嫌", "弃", "鄙", "蔑",
|
|
68
|
-
"轻",
|
|
68
|
+
"轻", "珍", "爱", "惜", "怜", "悯", "疼", "宠", "溺", "娇", "惯", "纵", "容",
|
|
69
69
|
"包", "庇", "袒", "呵", "佑", "祝", "福", "祈", "祷", "祭", "祀", "拜", "念", "诵",
|
|
70
70
|
"咏", "吟", "唱", "歌", "舞", "蹈", "旋", "滚", "滑", "溜", "窜", "遁", "隐", "匿",
|
|
71
71
|
"潜", "伏", "修", "行", "悟", "参", "禅", "坐", "定", "成", "化", "属", "于", "存",
|
|
@@ -74,8 +74,8 @@ const ACTION_VERBS = [
|
|
|
74
74
|
"派", "颁", "授", "予", "赠", "托", "委", "信", "任", "依", "凭", "仗", "恃", "仰",
|
|
75
75
|
"指", "盼", "期", "渴", "奢", "妄", "幻", "梦", "理", "设", "构", "揣", "猜", "预",
|
|
76
76
|
"料", "征", "兆", "迹", "象", "端", "倪", "苗", "头", "绪", "脉", "络", "纹", "层",
|
|
77
|
-
"次", "框", "架", "体",
|
|
78
|
-
|
|
77
|
+
"次", "框", "架", "体", "程", "序", "骤", "环", "节", "细",
|
|
78
|
+
"难", "焦", "热", "卖", "痛", "肋", "弱", "短", "板", "瓶",
|
|
79
79
|
"颈", "壁", "垒", "藩", "篱", "桎", "梏", "枷", "锁", "镣", "牢", "笼", "樊", "囚",
|
|
80
80
|
"罩", "络"
|
|
81
81
|
];
|
|
@@ -89,9 +89,10 @@ const ACTION_SET = new Set(ACTION_VERBS);
|
|
|
89
89
|
/** 切句:按句末标点切分。 */
|
|
90
90
|
function splitSentences(text) {
|
|
91
91
|
// v3.5.0 #50b:标点切句 + 无标点换行也切(逐行排版正确分句);未完句判定按"行末豁免"处理(见 measureStyleMetrics)
|
|
92
|
+
// v3.5.0 H3:句末标点后吞闭合引号("她说:\u201c走吧。\u201d" 不再切成孤儿引号句)
|
|
92
93
|
return String(text)
|
|
93
94
|
.split(/(?<=[。!?!?])\s*|\n+/)
|
|
94
|
-
.map(function (s) { return s.trim(); })
|
|
95
|
+
.map(function (s) { return s.replace(/[”」』’\"']+$/, "").trim(); })
|
|
95
96
|
.filter(function (s) { return s.length > 0; });
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -113,7 +114,8 @@ export function measureStyleMetrics(text) {
|
|
|
113
114
|
const complexity = sentences.length > 0 ? smallClauseTotal / sentences.length : 0;
|
|
114
115
|
|
|
115
116
|
// 2) 修饰密度:X的 / X地(“的/地”前的 1-4 字修饰语)
|
|
116
|
-
|
|
117
|
+
// v3.6.0:「目的/的确/的的确确」的"的"不是修饰助词——lookbehind 排除(真修饰语不受影响)
|
|
118
|
+
const adjMods = (t.match(/[\u4e00-\u9fff]{1,3}(?<!目)(?<!的)的/g) || []).length;
|
|
117
119
|
const advMods = (t.match(/[\u4e00-\u9fff]{1,4}地/g) || []).length;
|
|
118
120
|
const modifierDensity = per1000(adjMods + advMods);
|
|
119
121
|
|
|
@@ -168,10 +170,13 @@ export function measureStyleMetrics(text) {
|
|
|
168
170
|
const dash = (t.match(/——/g) || []).length;
|
|
169
171
|
const gapChars = per1000(ellipsis * 2 + dash * 2);
|
|
170
172
|
// v3.5.0 #50b:未完句判定——省略号结尾或行内无句末标点;行末无标点(排版分行)视为完整句,不再虚高 gapIndex
|
|
173
|
+
// v3.5.0 L1:顺序游标定位(重复/子串文本不再误判——"他走了。他走了\n"第二句正确豁免)
|
|
174
|
+
let searchFrom = 0;
|
|
171
175
|
const unfinished = sentences.filter(function (s) {
|
|
176
|
+
const idxU = t.indexOf(s, searchFrom);
|
|
177
|
+
if (idxU !== -1) searchFrom = idxU + s.length;
|
|
172
178
|
if (/[……—…]$/.test(s)) return true;
|
|
173
179
|
if (/[。!?!?]$/.test(s)) return false;
|
|
174
|
-
const idxU = t.indexOf(s);
|
|
175
180
|
if (idxU === -1) return true;
|
|
176
181
|
const afterU = t[idxU + s.length];
|
|
177
182
|
if (afterU === undefined || afterU === "\n" || afterU === "\r") return false;
|
package/lib/vibe.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* v2.1.0 气质聚合层:把插件各工具的已有输出(detect 词频/题材、情感净化、
|
|
3
|
-
* 情感量化 V/Δ/C、意象极性/歧义、语义隐性情感)汇总成"氛围光谱"(
|
|
3
|
+
* 情感量化 V/Δ/C、意象极性/歧义、语义隐性情感)汇总成"氛围光谱"(12 轴)。
|
|
4
4
|
* 纯规则加权 0 token;不做"贴标签",只输出数值坐标 + 组合结论 + 证据链。
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -44,14 +44,16 @@ function freqIn(evArr, words) {
|
|
|
44
44
|
/** 语义隐性情感分布 → 各轴信号 */
|
|
45
45
|
function semSignals(dist) {
|
|
46
46
|
const s = { nightmare: 0, angst: 0, heartwarming: 0, tearjerker: 0, dark: 0, mystery: 0, blaze: 0, absurd: 0, lonesome: 0, fluff: 0, aesthetic: 0, sensual: 0 };
|
|
47
|
-
// 键与 embedding.js IMPLICIT_EMOTION_PROTOTYPES 的
|
|
47
|
+
// 键与 embedding.js IMPLICIT_EMOTION_PROTOTYPES 的 29 个 emotion 标签严格对齐(v2.5 修复)
|
|
48
48
|
const map = {
|
|
49
49
|
脆弱: "nightmare", 恐惧: "nightmare", 不安: "nightmare",
|
|
50
50
|
焦虑: "angst", 隐忍: "angst", "压抑的愤怒": "angst", 无奈: "angst",
|
|
51
51
|
温暖: "heartwarming", 温柔: "heartwarming", 幸福: "heartwarming", 甜蜜: "heartwarming", 释然: "heartwarming", 仰慕: "heartwarming",
|
|
52
52
|
失落: "tearjerker", 悲伤: "tearjerker", 怅惘: "tearjerker", 心碎: "tearjerker", 不舍: "tearjerker", 眷恋: "tearjerker", 苦涩: "tearjerker",
|
|
53
53
|
孤独: "lonesome", 疏离: "lonesome", 决绝: "lonesome",
|
|
54
|
-
厌恶: "dark", 震惊: "absurd"
|
|
54
|
+
厌恶: "dark", 震惊: "absurd",
|
|
55
|
+
// v3.5.0 M7:近义近似映射(原型无 4 轴域,语义信号不再恒 0)
|
|
56
|
+
甜宠: "fluff", 悬疑: "mystery", 唯美: "aesthetic", 情欲: "sensual"
|
|
55
57
|
};
|
|
56
58
|
for (const [k, v] of Object.entries(dist || {})) {
|
|
57
59
|
const axis = map[k];
|
|
@@ -77,6 +79,10 @@ const WEB_NOVEL_SIGNALS = {
|
|
|
77
79
|
aesthetic: ["月色", "清辉", "烟雨", "荷塘", "落英", "余韵", "浮光", "静默", "素净", "微凉", "风过", "细碎"],
|
|
78
80
|
sensual: ["呼吸", "发烫", "贴近", "肌肤", "颤栗", "酥麻", "灼热", "喘息", "缠绕", "柔软", "耳畔", "温存"]
|
|
79
81
|
};
|
|
82
|
+
// v3.5.0 H1b:词表统一剥 ★ 字尾(模块加载时一次;★ 是词源标注,带星号永不命中——61 个词大面积作废)
|
|
83
|
+
for (const axis of Object.keys(WEB_NOVEL_SIGNALS)) {
|
|
84
|
+
WEB_NOVEL_SIGNALS[axis] = WEB_NOVEL_SIGNALS[axis].map(function (w) { return w.replace(/★$/, ""); });
|
|
85
|
+
}
|
|
80
86
|
const THEME_AXIS_LINK = [
|
|
81
87
|
{ themes: ["豪门总裁", "总裁", "现言"], axis: "fluff", bonus: 0.2, needBase: 0.1 },
|
|
82
88
|
{ themes: ["虐恋", "替身"], axis: "tearjerker", bonus: 0.25, needBase: 0.1 },
|
|
@@ -121,7 +127,7 @@ export function computeVibe(detect, emotion, text = "") {
|
|
|
121
127
|
// v2.2.0 网文信号:动作/套路词群扫描(封顶计分)
|
|
122
128
|
// v2.6.0 提速:原 12 轴×164 词全文本 indexOf 扫描 → 先滑窗统计 2-4 字词频(Map),词表命中改查表
|
|
123
129
|
// v3.5.0 #58:只生成词表实际词长的 n-gram(旧版逐位生成 2/3/4 字 ≈ 3×字数条目,50 万字书 ~150 万键)
|
|
124
|
-
|
|
130
|
+
// v3.5.0 R5(#58):滑窗只生成 2-4 字 n-gram(词表 >4 字词低频,单独 indexOf 查,不放大内存)
|
|
125
131
|
const longWords = new Set();
|
|
126
132
|
const signalLens = new Set();
|
|
127
133
|
for (const axisWords of Object.values(WEB_NOVEL_SIGNALS)) {
|
package/package.json
CHANGED
|
@@ -125,7 +125,7 @@ novel_sentence_analysis 的 emotion.quantification.semanticImplicit(semanticEm
|
|
|
125
125
|
- 分析/续写前先跑 novel_style_report:输出 6 维测量数据(文风指纹/高频词汇/题材流派/情感量化/氛围光谱 12 轴/语义风格距离),插件不贴标签;
|
|
126
126
|
- **风格基线(v3.0.0)**:novel_style_report 额外输出「文笔六维基线带」(句法复杂度/修饰密度/抽象度/动作密度/不确定性/留白指数,按章节 μ±σ);
|
|
127
127
|
原创或风格模仿时:① 先读原书基线带 → ② 动笔前声明本书基线(默认取原书 μ±σ,可声明有意偏移,如“节奏加快一档”)→ ③ 每章写完跑 novel_style_check,对照「文笔六维对照」节,出带(超出 ±容差)的维度定向修正后再续写;
|
|
128
|
-
容差带(允许低于/高于基线的百分比)可在侧边栏「写作助手功能 → 风格基线」调整,默认推荐 = 原著章节波动的 1.5σ(每维独立,下限 ±
|
|
128
|
+
容差带(允许低于/高于基线的百分比)可在侧边栏「写作助手功能 → 风格基线」调整,默认推荐 = 原著章节波动的 1.5σ(每维独立,下限 ±10%~100%(推荐值自动取整));主题/情节/人物完全自由,基线只管写法层面。
|
|
129
129
|
- AI 结合数据给出风格气质判断,通过 aiJudgment 参数回传(插件存入 .novel-writer/style-reports/,后续 action=get 读取);续写时参考已保存判断保持风格一致。
|
|
130
130
|
|
|
131
131
|
## 原创模式(v3.1.0)
|