gwe-engine 3.2.0 → 3.3.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/dist/cli.js +458 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +163 -1
- package/dist/index.js +427 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -10257,8 +10257,384 @@ function createEngineWithKB(provider) {
|
|
|
10257
10257
|
return { engine, result };
|
|
10258
10258
|
}
|
|
10259
10259
|
|
|
10260
|
+
// src/book-context.ts
|
|
10261
|
+
var SENSORY_SINGLE = /^(疼|麻|冷|热|烫|酸|胀|痒|涩|苦|甜|咸|腥|臭|香|黑|亮|静|响|湿|干|硬|软|滑|糙|重|轻)。?$/;
|
|
10262
|
+
var DIALOGUE_OPEN = /^[""「"]/;
|
|
10263
|
+
var NEGATION_REVEAL = /不是[^。?!]{1,8}[。?!]\s*是/g;
|
|
10264
|
+
function detectOpeningPattern(firstSentence) {
|
|
10265
|
+
const trimmed = firstSentence.trim();
|
|
10266
|
+
const len = trimmed.length;
|
|
10267
|
+
let type = "description";
|
|
10268
|
+
let keyword = trimmed.slice(0, 4);
|
|
10269
|
+
let signature = "";
|
|
10270
|
+
if (SENSORY_SINGLE.test(trimmed) || len <= 3 && /[疼麻冷热烫酸胀痛痒涩]/.test(trimmed)) {
|
|
10271
|
+
type = "single-sensory";
|
|
10272
|
+
keyword = trimmed.replace(/[。!?]/g, "");
|
|
10273
|
+
signature = `sensory:${keyword}`;
|
|
10274
|
+
} else if (DIALOGUE_OPEN.test(trimmed)) {
|
|
10275
|
+
type = "dialogue";
|
|
10276
|
+
signature = "dialogue";
|
|
10277
|
+
} else if (/^(他|她|我|[\u4e00-\u9fa5]{2,3})(把|将|用|举|拿|握|伸|抬|踢|踩|抓|放|推|拉|扯|拽|蹲|站|跳|跑|走|转身|回头|低头|抬头)/.test(trimmed)) {
|
|
10278
|
+
type = "action";
|
|
10279
|
+
signature = `action:${trimmed.slice(0, 2)}`;
|
|
10280
|
+
} else if (/^(我|他|她)(想|觉得|意识到|知道|明白|想起|记得|感觉|以为|怀疑)/.test(trimmed)) {
|
|
10281
|
+
type = "internal-thought";
|
|
10282
|
+
signature = "internal";
|
|
10283
|
+
} else {
|
|
10284
|
+
type = "description";
|
|
10285
|
+
signature = `desc:${len}`;
|
|
10286
|
+
}
|
|
10287
|
+
return { length: len, type, keyword, signature };
|
|
10288
|
+
}
|
|
10289
|
+
function detectEndingPattern(lastSentences) {
|
|
10290
|
+
const fullEnding = lastSentences.join("");
|
|
10291
|
+
const lastSent = lastSentences[lastSentences.length - 1]?.trim() || "";
|
|
10292
|
+
const negationMatches = fullEnding.match(NEGATION_REVEAL);
|
|
10293
|
+
const usesNegationReveal = negationMatches !== null && negationMatches.length >= 1;
|
|
10294
|
+
const fragmentCount = lastSentences.filter((s) => s.trim().length <= 8).length;
|
|
10295
|
+
let type = "action";
|
|
10296
|
+
if (usesNegationReveal && /(是|原来|其实)/.test(fullEnding.slice(-20))) {
|
|
10297
|
+
type = "reveal";
|
|
10298
|
+
} else if (/[??]|吗|呢|难道|会不会|是不是/.test(lastSent)) {
|
|
10299
|
+
type = "cliffhanger";
|
|
10300
|
+
} else if (DIALOGUE_OPEN.test(lastSent)) {
|
|
10301
|
+
type = "dialogue";
|
|
10302
|
+
} else if (usesNegationReveal) {
|
|
10303
|
+
type = "reveal";
|
|
10304
|
+
} else if (fragmentCount >= 2 && lastSent.length <= 6) {
|
|
10305
|
+
type = "emotion";
|
|
10306
|
+
} else {
|
|
10307
|
+
type = "action";
|
|
10308
|
+
}
|
|
10309
|
+
const signature = `${type}:${usesNegationReveal ? "neg" : "normal"}:${fragmentCount}`;
|
|
10310
|
+
return {
|
|
10311
|
+
type,
|
|
10312
|
+
usesNegationReveal,
|
|
10313
|
+
fragmentCount,
|
|
10314
|
+
lastSentenceLength: lastSent.length,
|
|
10315
|
+
signature
|
|
10316
|
+
};
|
|
10317
|
+
}
|
|
10318
|
+
function extractSettingRules(text, chapterIndex) {
|
|
10319
|
+
const rules = [];
|
|
10320
|
+
const absolutePatterns = [
|
|
10321
|
+
/([^,。?!\n]{2,15})永远(?:是|不会|不能|不可能)([^,。?!\n]{2,15})/g,
|
|
10322
|
+
/([^,。?!\n]{2,15})从来(?:不|没|没有)([^,。?!\n]{2,15})/g,
|
|
10323
|
+
/([^,。?!\n]{2,15})(?:绝?对|一定|必须)是([^,。?!\n]{2,15})/g,
|
|
10324
|
+
/([^,。?!\n]{2,8})只(?:能|会|有)([^,。?!\n]{2,15})/g
|
|
10325
|
+
];
|
|
10326
|
+
for (const pattern of absolutePatterns) {
|
|
10327
|
+
let match;
|
|
10328
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
10329
|
+
rules.push({
|
|
10330
|
+
rule: match[0],
|
|
10331
|
+
type: "absolute",
|
|
10332
|
+
keywords: [match[1].slice(-4), match[2].slice(0, 4)],
|
|
10333
|
+
establishedIn: chapterIndex
|
|
10334
|
+
});
|
|
10335
|
+
}
|
|
10336
|
+
}
|
|
10337
|
+
return rules;
|
|
10338
|
+
}
|
|
10339
|
+
function extractForeshadowing(text, chapterIndex) {
|
|
10340
|
+
const foreshadows = [];
|
|
10341
|
+
const plantPatterns = [
|
|
10342
|
+
{ re: /([^,。?!\n]{0,10}(?:想起|记得|回忆起|意识到|感觉到|注意到)[^,。?!\n]{2,25})/g, importance: 2 },
|
|
10343
|
+
{ re: /([^,。?!\n]{0,10}(?:说过|警告过|提醒过|告诉过他)[^,。?!\n]{2,25})/g, importance: 3 },
|
|
10344
|
+
{ re: /(不对劲|有问题|不对|奇怪|异常|反常|不寻常)/g, importance: 2 }
|
|
10345
|
+
];
|
|
10346
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10347
|
+
for (const { re, importance } of plantPatterns) {
|
|
10348
|
+
let match;
|
|
10349
|
+
while ((match = re.exec(text)) !== null) {
|
|
10350
|
+
const desc = match[1] || match[0];
|
|
10351
|
+
const key = desc.slice(0, 10);
|
|
10352
|
+
if (seen.has(key)) continue;
|
|
10353
|
+
seen.add(key);
|
|
10354
|
+
foreshadows.push({
|
|
10355
|
+
keyword: key,
|
|
10356
|
+
description: desc.slice(0, 40),
|
|
10357
|
+
plantedIn: chapterIndex,
|
|
10358
|
+
importance,
|
|
10359
|
+
resolved: false
|
|
10360
|
+
});
|
|
10361
|
+
}
|
|
10362
|
+
}
|
|
10363
|
+
return foreshadows;
|
|
10364
|
+
}
|
|
10365
|
+
var BookContext = class {
|
|
10366
|
+
chapters = [];
|
|
10367
|
+
allSettingRules = [];
|
|
10368
|
+
allForeshadowing = [];
|
|
10369
|
+
globalCharacterStates = /* @__PURE__ */ new Map();
|
|
10370
|
+
/** 添加一个章节的快照,返回检测到的全书级别问题 */
|
|
10371
|
+
addChapter(snapshot) {
|
|
10372
|
+
const issues = [];
|
|
10373
|
+
const openingIssues = this.checkOpeningRepetition(snapshot);
|
|
10374
|
+
issues.push(...openingIssues);
|
|
10375
|
+
const endingIssues = this.checkEndingRepetition(snapshot);
|
|
10376
|
+
issues.push(...endingIssues);
|
|
10377
|
+
const settingIssues = this.checkSettingViolations(snapshot);
|
|
10378
|
+
issues.push(...settingIssues);
|
|
10379
|
+
const continuityIssues = this.checkContinuity(snapshot);
|
|
10380
|
+
issues.push(...continuityIssues);
|
|
10381
|
+
const foreshadowIssues = this.checkForeshadowing(snapshot);
|
|
10382
|
+
issues.push(...foreshadowIssues);
|
|
10383
|
+
this.chapters.push(snapshot);
|
|
10384
|
+
for (const rule of snapshot.settingRules) {
|
|
10385
|
+
this.allSettingRules.push(rule);
|
|
10386
|
+
}
|
|
10387
|
+
for (const fs2 of snapshot.foreshadowing) {
|
|
10388
|
+
this.allForeshadowing.push(fs2);
|
|
10389
|
+
}
|
|
10390
|
+
for (const [name, state] of snapshot.characterStates) {
|
|
10391
|
+
this.globalCharacterStates.set(name, state);
|
|
10392
|
+
}
|
|
10393
|
+
return issues;
|
|
10394
|
+
}
|
|
10395
|
+
/** 检测最近N章开头模式是否重复 */
|
|
10396
|
+
checkOpeningRepetition(snapshot) {
|
|
10397
|
+
const issues = [];
|
|
10398
|
+
const recentN = 5;
|
|
10399
|
+
const recent = this.chapters.slice(-recentN);
|
|
10400
|
+
const sameTypeCount = recent.filter((c) => c.openingPattern.type === snapshot.openingPattern.type).length;
|
|
10401
|
+
const sameSensoryCount = recent.filter((c) => c.openingPattern.type === "single-sensory").length;
|
|
10402
|
+
if (sameSensoryCount >= 1 && snapshot.openingPattern.type === "single-sensory") {
|
|
10403
|
+
issues.push({
|
|
10404
|
+
level: sameSensoryCount >= 2 ? "warning" : "info",
|
|
10405
|
+
type: "repetitive-opening",
|
|
10406
|
+
message: `${sameSensoryCount >= 2 ? "\u8FDE\u7EED" : ""}${sameSensoryCount + 1}\u7AE0\u4F7F\u7528\u5355\u5B57\u611F\u5B98\u5F00\u5934\uFF08"${snapshot.firstSentence.slice(0, 8)}"\uFF09${sameSensoryCount >= 2 ? "\uFF0C\u5BB9\u6613\u5957\u8DEF\u5316" : ""}\u3002\u5EFA\u8BAE\u6362\u5BF9\u8BDD/\u52A8\u4F5C\u5F00\u5934\u3002`,
|
|
10407
|
+
chapterIndex: snapshot.index
|
|
10408
|
+
});
|
|
10409
|
+
}
|
|
10410
|
+
if (sameTypeCount >= 2 && snapshot.openingPattern.type !== "single-sensory") {
|
|
10411
|
+
issues.push({
|
|
10412
|
+
level: "info",
|
|
10413
|
+
type: "repetitive-opening",
|
|
10414
|
+
message: `\u8FDE\u7EED${sameTypeCount + 1}\u7AE0\u4F7F\u7528${snapshot.openingPattern.type}\u7C7B\u578B\u5F00\u5934\uFF0C\u5EFA\u8BAE\u53D8\u5316\u8282\u594F\u3002`,
|
|
10415
|
+
chapterIndex: snapshot.index
|
|
10416
|
+
});
|
|
10417
|
+
}
|
|
10418
|
+
return issues;
|
|
10419
|
+
}
|
|
10420
|
+
/** 检测最近N章结尾模式是否重复 */
|
|
10421
|
+
checkEndingRepetition(snapshot) {
|
|
10422
|
+
const issues = [];
|
|
10423
|
+
const recentN = 5;
|
|
10424
|
+
const recent = this.chapters.slice(-recentN);
|
|
10425
|
+
const sameEndingCount = recent.filter((c) => c.endingPattern.signature === snapshot.endingPattern.signature).length;
|
|
10426
|
+
const sameNegationCount = recent.filter((c) => c.endingPattern.usesNegationReveal).length;
|
|
10427
|
+
if (sameNegationCount >= 1 && snapshot.endingPattern.usesNegationReveal) {
|
|
10428
|
+
issues.push({
|
|
10429
|
+
level: sameNegationCount >= 2 ? "warning" : "info",
|
|
10430
|
+
type: "repetitive-ending",
|
|
10431
|
+
message: `${sameNegationCount >= 2 ? "\u8FDE\u7EED" : ""}${sameNegationCount + 1}\u7AE0\u4F7F\u7528"\u4E0D\u662FX\u3002\u662FY\u3002"\u7684\u5426\u5B9A\u63ED\u793A\u7ED3\u5C3E${sameNegationCount >= 2 ? "\uFF0C\u5957\u8DEF\u611F\u5F3A" : ""}\u3002\u5EFA\u8BAE\u6362\u4E2A\u7ED3\u5C3E\u65B9\u5F0F\uFF08\u5BF9\u8BDD/\u52A8\u4F5C/\u60AC\u5FF5\u63D0\u95EE\uFF09\u3002`,
|
|
10432
|
+
chapterIndex: snapshot.index
|
|
10433
|
+
});
|
|
10434
|
+
}
|
|
10435
|
+
if (sameEndingCount >= 2 && snapshot.endingPattern.fragmentCount >= 3) {
|
|
10436
|
+
issues.push({
|
|
10437
|
+
level: "info",
|
|
10438
|
+
type: "repetitive-ending",
|
|
10439
|
+
message: `\u8FDE\u7EED${sameEndingCount + 1}\u7AE0\u4F7F\u7528\u591A\u6BB5\u65AD\u53E5\u6536\u5C3E\uFF08${snapshot.endingPattern.fragmentCount}\u4E2A\u77ED\u53E5\uFF09\uFF0C\u6CE8\u610F\u53D8\u5316\u3002`,
|
|
10440
|
+
chapterIndex: snapshot.index
|
|
10441
|
+
});
|
|
10442
|
+
}
|
|
10443
|
+
return issues;
|
|
10444
|
+
}
|
|
10445
|
+
/** 检测本章是否违反之前建立的设定 */
|
|
10446
|
+
checkSettingViolations(snapshot) {
|
|
10447
|
+
const issues = [];
|
|
10448
|
+
for (const rule of this.allSettingRules) {
|
|
10449
|
+
const { keywords, rule: ruleText, establishedIn } = rule;
|
|
10450
|
+
if (keywords.length < 2) continue;
|
|
10451
|
+
const isNegative = /不|没|从未|永不/.test(ruleText);
|
|
10452
|
+
if (!isNegative) continue;
|
|
10453
|
+
const [subject, predicate] = keywords;
|
|
10454
|
+
const violationRegex = new RegExp(`${subject}[^\uFF0C\u3002\uFF1F\uFF01]{0,15}${predicate.replace(/不|没/g, "")}`);
|
|
10455
|
+
}
|
|
10456
|
+
return issues;
|
|
10457
|
+
}
|
|
10458
|
+
/** 检测与上一章的衔接是否断裂 */
|
|
10459
|
+
checkContinuity(snapshot) {
|
|
10460
|
+
const issues = [];
|
|
10461
|
+
if (this.chapters.length === 0) return issues;
|
|
10462
|
+
const prev = this.chapters[this.chapters.length - 1];
|
|
10463
|
+
const prevScene = prev.closingScene;
|
|
10464
|
+
const firstSent = snapshot.firstSentence;
|
|
10465
|
+
const openContext = firstSent.slice(0, 50);
|
|
10466
|
+
const hasCharacterLink = [...snapshot.characterStates.keys()].some(
|
|
10467
|
+
(name) => prev.characterStates.has(name)
|
|
10468
|
+
);
|
|
10469
|
+
if (!hasCharacterLink && prev.characterStates.size > 0 && snapshot.characterStates.size > 0) {
|
|
10470
|
+
const prevNames = [...prev.characterStates.keys()].slice(0, 3).join("\u3001");
|
|
10471
|
+
issues.push({
|
|
10472
|
+
level: "warning",
|
|
10473
|
+
type: "continuity-break",
|
|
10474
|
+
message: `\u4E0A\u7AE0\u51FA\u573A\u4EBA\u7269\uFF08${prevNames}\uFF09\u5728\u672C\u7AE0\u5F00\u5934\u5747\u672A\u51FA\u73B0\uFF0C\u6CE8\u610F\u573A\u666F\u8F6C\u6362\u662F\u5426\u81EA\u7136\u3002`,
|
|
10475
|
+
chapterIndex: snapshot.index
|
|
10476
|
+
});
|
|
10477
|
+
}
|
|
10478
|
+
return issues;
|
|
10479
|
+
}
|
|
10480
|
+
/** 追踪伏笔,标记长期未回收的伏笔 */
|
|
10481
|
+
checkForeshadowing(snapshot) {
|
|
10482
|
+
const issues = [];
|
|
10483
|
+
const currentText = snapshot.lastSentences.join("") + snapshot.firstSentence;
|
|
10484
|
+
for (const fs2 of this.allForeshadowing) {
|
|
10485
|
+
if (fs2.resolved) continue;
|
|
10486
|
+
if (currentText.includes(fs2.keyword.slice(0, 3))) {
|
|
10487
|
+
fs2.resolved = true;
|
|
10488
|
+
}
|
|
10489
|
+
}
|
|
10490
|
+
for (const fs2 of this.allForeshadowing) {
|
|
10491
|
+
if (fs2.resolved) continue;
|
|
10492
|
+
const gap = snapshot.index - fs2.plantedIn;
|
|
10493
|
+
if (fs2.importance >= 3 && gap >= 5) {
|
|
10494
|
+
issues.push({
|
|
10495
|
+
level: "warning",
|
|
10496
|
+
type: "unresolved-foreshadow",
|
|
10497
|
+
message: `\u91CD\u8981\u4F0F\u7B14"${fs2.keyword}\u2026\u2026"\u5728\u7B2C${fs2.plantedIn + 1}\u7AE0\u57CB\u8BBE\uFF0C\u5DF2\u8FC7${gap}\u7AE0\u672A\u56DE\u6536\u3002`,
|
|
10498
|
+
chapterIndex: snapshot.index,
|
|
10499
|
+
details: fs2.description
|
|
10500
|
+
});
|
|
10501
|
+
} else if (fs2.importance >= 2 && gap >= 10) {
|
|
10502
|
+
issues.push({
|
|
10503
|
+
level: "info",
|
|
10504
|
+
type: "stale-thread",
|
|
10505
|
+
message: `\u4F0F\u7B14"${fs2.keyword}\u2026\u2026"\u5DF2\u8FC7${gap}\u7AE0\u672A\u56DE\u6536\uFF0C\u53EF\u80FD\u5DF2\u88AB\u9057\u5FD8\u3002`,
|
|
10506
|
+
chapterIndex: snapshot.index
|
|
10507
|
+
});
|
|
10508
|
+
}
|
|
10509
|
+
}
|
|
10510
|
+
return issues;
|
|
10511
|
+
}
|
|
10512
|
+
getChapterCount() {
|
|
10513
|
+
return this.chapters.length;
|
|
10514
|
+
}
|
|
10515
|
+
getStats() {
|
|
10516
|
+
const unresolved = this.allForeshadowing.filter((f) => !f.resolved).length;
|
|
10517
|
+
const total = this.allForeshadowing.length;
|
|
10518
|
+
return {
|
|
10519
|
+
chapters: this.chapters.length,
|
|
10520
|
+
totalForeshadowing: total,
|
|
10521
|
+
unresolvedForeshadowing: unresolved,
|
|
10522
|
+
settingRules: this.allSettingRules.length,
|
|
10523
|
+
characters: this.globalCharacterStates.size
|
|
10524
|
+
};
|
|
10525
|
+
}
|
|
10526
|
+
};
|
|
10527
|
+
|
|
10528
|
+
// src/book-checker.ts
|
|
10529
|
+
var CHAPTER_HEADER = /第[一二三四五六七八九十百千万零\d]+[章节回卷部][\s\n]*/g;
|
|
10530
|
+
function splitChapters(text) {
|
|
10531
|
+
const matches = [...text.matchAll(CHAPTER_HEADER)];
|
|
10532
|
+
if (matches.length === 0) {
|
|
10533
|
+
return [{ title: "\u5168\u6587", content: text.trim(), index: 0 }];
|
|
10534
|
+
}
|
|
10535
|
+
const chapters = [];
|
|
10536
|
+
for (let i = 0; i < matches.length; i++) {
|
|
10537
|
+
const match = matches[i];
|
|
10538
|
+
const titleStart = match.index ?? 0;
|
|
10539
|
+
const contentStart = titleStart + match[0].length;
|
|
10540
|
+
const contentEnd = i + 1 < matches.length ? matches[i + 1].index ?? text.length : text.length;
|
|
10541
|
+
const title = match[0].trim();
|
|
10542
|
+
const content = text.slice(contentStart, contentEnd).trim();
|
|
10543
|
+
if (content.length > 30) {
|
|
10544
|
+
chapters.push({ title, content, index: chapters.length });
|
|
10545
|
+
}
|
|
10546
|
+
}
|
|
10547
|
+
return chapters.length > 0 ? chapters : [{ title: "\u5168\u6587", content: text.trim(), index: 0 }];
|
|
10548
|
+
}
|
|
10549
|
+
function extractChapterSnapshot(text, index, title = `\u7B2C${index + 1}\u7AE0`) {
|
|
10550
|
+
const sentences = text.replace(/\r\n/g, "\n").split(/(?<=[。!?…])\s*\n?|(?<=[。!?])\s+/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
10551
|
+
const firstSentence = sentences[0] || text.slice(0, 20);
|
|
10552
|
+
const lastSentences = sentences.slice(-5);
|
|
10553
|
+
const nameRegex = /[\u4e00-\u9fa5]{2,3}(?=说|道|问|喊|叫|笑|怒|叹|想|看|听|走|跑|站|蹲|转身|回头|点头|摇头|皱眉|咬|握|拿|举|伸|抬)/g;
|
|
10554
|
+
const nameCounts = /* @__PURE__ */ new Map();
|
|
10555
|
+
let m;
|
|
10556
|
+
while ((m = nameRegex.exec(text)) !== null) {
|
|
10557
|
+
const name = m[0];
|
|
10558
|
+
if (/^(自己|大家|众人|对方|这个|那个|什么|怎么|没有|不是|可以|已经|突然|然后|可是|但是|如果|因为|所以|虽然|只是|就是|还是|还有|有些|一点|一下|一声|一眼|脸上|心里|眼中|时候|地方|东西|声音|样子|问题|事情)/.test(name)) continue;
|
|
10559
|
+
nameCounts.set(name, (nameCounts.get(name) || 0) + 1);
|
|
10560
|
+
}
|
|
10561
|
+
const characterStates = /* @__PURE__ */ new Map();
|
|
10562
|
+
for (const [name, count] of nameCounts) {
|
|
10563
|
+
if (count >= 2) {
|
|
10564
|
+
const lastIdx = text.lastIndexOf(name);
|
|
10565
|
+
const context = text.slice(Math.max(0, lastIdx - 10), Math.min(text.length, lastIdx + 30));
|
|
10566
|
+
characterStates.set(name, {
|
|
10567
|
+
name,
|
|
10568
|
+
lastLocation: "",
|
|
10569
|
+
lastAction: context.slice(0, 30),
|
|
10570
|
+
lastChapter: index
|
|
10571
|
+
});
|
|
10572
|
+
}
|
|
10573
|
+
}
|
|
10574
|
+
const endingText = text.slice(-200);
|
|
10575
|
+
const closingScene = endingText.slice(0, 50);
|
|
10576
|
+
const settingRules = extractSettingRules(text, index);
|
|
10577
|
+
const foreshadowing = extractForeshadowing(text, index);
|
|
10578
|
+
return {
|
|
10579
|
+
index,
|
|
10580
|
+
title,
|
|
10581
|
+
firstSentence,
|
|
10582
|
+
openingPattern: detectOpeningPattern(firstSentence),
|
|
10583
|
+
lastSentences,
|
|
10584
|
+
endingPattern: detectEndingPattern(lastSentences),
|
|
10585
|
+
characterStates,
|
|
10586
|
+
settingRules,
|
|
10587
|
+
foreshadowing,
|
|
10588
|
+
resolvedForeshadow: [],
|
|
10589
|
+
closingScene,
|
|
10590
|
+
charCount: text.length
|
|
10591
|
+
};
|
|
10592
|
+
}
|
|
10593
|
+
function checkBook(fullText) {
|
|
10594
|
+
const chapters = splitChapters(fullText);
|
|
10595
|
+
const ctx = new BookContext();
|
|
10596
|
+
const allIssues = [];
|
|
10597
|
+
const snapshots = [];
|
|
10598
|
+
let totalChars = 0;
|
|
10599
|
+
const openingCounts = {};
|
|
10600
|
+
const endingCounts = {};
|
|
10601
|
+
let repOpen = 0, repEnd = 0, setVio = 0, contBrk = 0;
|
|
10602
|
+
for (const { title, content, index } of chapters) {
|
|
10603
|
+
const snap = extractChapterSnapshot(content, index, title);
|
|
10604
|
+
snapshots.push(snap);
|
|
10605
|
+
totalChars += snap.charCount;
|
|
10606
|
+
openingCounts[snap.openingPattern.type] = (openingCounts[snap.openingPattern.type] || 0) + 1;
|
|
10607
|
+
endingCounts[snap.endingPattern.type] = (endingCounts[snap.endingPattern.type] || 0) + 1;
|
|
10608
|
+
const issues = ctx.addChapter(snap);
|
|
10609
|
+
for (const issue of issues) {
|
|
10610
|
+
allIssues.push(issue);
|
|
10611
|
+
if (issue.type === "repetitive-opening") repOpen++;
|
|
10612
|
+
if (issue.type === "repetitive-ending") repEnd++;
|
|
10613
|
+
if (issue.type === "setting-violation") setVio++;
|
|
10614
|
+
if (issue.type === "continuity-break") contBrk++;
|
|
10615
|
+
}
|
|
10616
|
+
}
|
|
10617
|
+
const stats = ctx.getStats();
|
|
10618
|
+
return {
|
|
10619
|
+
issues: allIssues,
|
|
10620
|
+
chapters: snapshots,
|
|
10621
|
+
stats: {
|
|
10622
|
+
totalChapters: chapters.length,
|
|
10623
|
+
totalChars,
|
|
10624
|
+
openingTypeCounts: openingCounts,
|
|
10625
|
+
endingTypeCounts: endingCounts,
|
|
10626
|
+
totalForeshadowing: stats.totalForeshadowing,
|
|
10627
|
+
unresolvedForeshadowing: stats.unresolvedForeshadowing,
|
|
10628
|
+
repetitiveOpenings: repOpen,
|
|
10629
|
+
repetitiveEndings: repEnd,
|
|
10630
|
+
settingViolations: setVio,
|
|
10631
|
+
continuityBreaks: contBrk
|
|
10632
|
+
}
|
|
10633
|
+
};
|
|
10634
|
+
}
|
|
10635
|
+
|
|
10260
10636
|
// src/cli.ts
|
|
10261
|
-
var VERSION = "3.
|
|
10637
|
+
var VERSION = "3.3.0";
|
|
10262
10638
|
var colors = {
|
|
10263
10639
|
reset: "\x1B[0m",
|
|
10264
10640
|
red: "\x1B[31m",
|
|
@@ -10278,7 +10654,8 @@ function printHelp() {
|
|
|
10278
10654
|
${colorize("GWE - Generic Web-novel Engine \u7F51\u6587\u8FFD\u8BFB\u529B\u5F15\u64CE", "bold")} v${VERSION}
|
|
10279
10655
|
|
|
10280
10656
|
${colorize("\u7528\u6CD5:", "cyan")}
|
|
10281
|
-
gwe check <file> \u68C0\u6D4B\
|
|
10657
|
+
gwe check <file> \u68C0\u6D4B\u5355\u4E2A\u7AE0\u8282\u6587\u4EF6
|
|
10658
|
+
gwe book <file> \u5168\u4E66\u68C0\u6D4B\uFF08\u591A\u7AE0\u8FDE\u8D2F\u6027/\u5957\u8DEF\u5316/\u4F0F\u7B14\uFF09
|
|
10282
10659
|
gwe - \u4ECE\u6807\u51C6\u8F93\u5165\u8BFB\u53D6\u6587\u672C\u5E76\u68C0\u6D4B
|
|
10283
10660
|
gwe --json <file> \u8F93\u51FAJSON\u683C\u5F0F\u7ED3\u679C\uFF08\u7528\u4E8E\u7A0B\u5E8F\u96C6\u6210\uFF09
|
|
10284
10661
|
gwe --help, -h \u663E\u793A\u5E2E\u52A9
|
|
@@ -10286,9 +10663,9 @@ ${colorize("\u7528\u6CD5:", "cyan")}
|
|
|
10286
10663
|
|
|
10287
10664
|
${colorize("\u793A\u4F8B:", "cyan")}
|
|
10288
10665
|
gwe check chapter.txt
|
|
10666
|
+
gwe book novel.txt
|
|
10289
10667
|
cat chapter.txt | gwe -
|
|
10290
10668
|
gwe --json chapter.txt > report.json
|
|
10291
|
-
echo "\u75BC\u3002\u51FF\u5C16\u7838\u8FDB\u6307\u9AA8..." | gwe -
|
|
10292
10669
|
|
|
10293
10670
|
${colorize("\u8BC4\u5206\u8BF4\u660E:", "cyan")}
|
|
10294
10671
|
\u226590\u5206 \u{1F3C6} \u4F18\u79C0\uFF0C\u5BF9\u6807\u4E00\u7EBF\u70ED\u95E8\u7F51\u6587
|
|
@@ -10300,6 +10677,9 @@ ${colorize("\u8BC4\u5206\u8BF4\u660E:", "cyan")}
|
|
|
10300
10677
|
${colorize("\u8BC4\u5206\u7EF4\u5EA6:", "cyan")}
|
|
10301
10678
|
\u8EAB\u4F53\u53CD\u5E94 \u611F\u5B98\u4FE1\u53F7 \u52A8\u4F5C\u63A8\u8FDB \u60C5\u7EEA\u5F20\u529B
|
|
10302
10679
|
\u4FE1\u606F\u63A8\u8FDB \u8F6C\u6298\u5BC6\u5EA6 \u7AE0\u672B\u94A9\u5B50
|
|
10680
|
+
|
|
10681
|
+
${colorize("\u5168\u4E66\u68C0\u6D4B:", "cyan")}
|
|
10682
|
+
\u68C0\u6D4B\u5F00\u5934/\u7ED3\u5C3E\u5957\u8DEF\u91CD\u590D\u3001\u7AE0\u8282\u8854\u63A5\u65AD\u88C2\u3001\u8BBE\u5B9A\u8FDD\u53CD\u3001\u4F0F\u7B14\u672A\u56DE\u6536
|
|
10303
10683
|
`);
|
|
10304
10684
|
}
|
|
10305
10685
|
function printVersion() {
|
|
@@ -10391,6 +10771,67 @@ function runCheck(text, filePath, outputJson2 = false) {
|
|
|
10391
10771
|
console.log(` \u586B\u5145\u8BCD: ${result.stats.fillerCount}\u4E2A \u5BF9\u8BDD\u5360\u6BD4: ${(result.stats.dialogueRatio * 100).toFixed(0)}%`);
|
|
10392
10772
|
console.log("");
|
|
10393
10773
|
}
|
|
10774
|
+
function printBookIssues(issues) {
|
|
10775
|
+
if (issues.length === 0) {
|
|
10776
|
+
console.log(colorize(" \u2713 \u5168\u4E66\u68C0\u6D4B\u901A\u8FC7\uFF0C\u672A\u53D1\u73B0\u8DE8\u7AE0\u95EE\u9898\uFF01", "green"));
|
|
10777
|
+
console.log("");
|
|
10778
|
+
return;
|
|
10779
|
+
}
|
|
10780
|
+
const errors = issues.filter((i) => i.level === "error");
|
|
10781
|
+
const warnings = issues.filter((i) => i.level === "warning");
|
|
10782
|
+
const infos = issues.filter((i) => i.level === "info");
|
|
10783
|
+
console.log(colorize(` \u2500\u2500\u2500 \u5168\u4E66\u95EE\u9898 (${colorize(`${errors.length} error`, "red")}, ${colorize(`${warnings.length} warning`, "yellow")}, ${colorize(`${infos.length} info`, "gray")}) \u2500\u2500\u2500`, "cyan"));
|
|
10784
|
+
console.log("");
|
|
10785
|
+
for (const issue of issues) {
|
|
10786
|
+
const icon = issue.level === "error" ? colorize(" \u2717", "red") : issue.level === "warning" ? colorize(" \u26A0", "yellow") : colorize(" \u2139", "gray");
|
|
10787
|
+
const ch = issue.chapterIndex !== void 0 ? colorize(`[\u7B2C${issue.chapterIndex + 1}\u7AE0]`, "cyan") : "";
|
|
10788
|
+
console.log(`${icon} ${ch} ${issue.message}`);
|
|
10789
|
+
console.log("");
|
|
10790
|
+
}
|
|
10791
|
+
}
|
|
10792
|
+
function runBookCheck(text, filePath) {
|
|
10793
|
+
console.log("");
|
|
10794
|
+
if (filePath) {
|
|
10795
|
+
console.log(colorize(` GWE V${VERSION} \u5168\u4E66\u8FDE\u8D2F\u6027\u68C0\u6D4B`, "bold") + colorize(` ${filePath}`, "gray"));
|
|
10796
|
+
} else {
|
|
10797
|
+
console.log(colorize(` GWE V${VERSION} \u5168\u4E66\u8FDE\u8D2F\u6027\u68C0\u6D4B`, "bold"));
|
|
10798
|
+
}
|
|
10799
|
+
console.log("");
|
|
10800
|
+
const result = checkBook(text);
|
|
10801
|
+
console.log(colorize(" \u2500\u2500\u2500 \u5168\u4E66\u6982\u89C8 \u2500\u2500\u2500", "cyan"));
|
|
10802
|
+
console.log(` \u7AE0\u8282\u6570: ${result.stats.totalChapters} \u603B\u5B57\u6570: ${result.stats.totalChars}\u5B57`);
|
|
10803
|
+
console.log(` \u4F0F\u7B14\u603B\u6570: ${result.stats.totalForeshadowing} \u672A\u56DE\u6536: ${colorize(String(result.stats.unresolvedForeshadowing), result.stats.unresolvedForeshadowing > 5 ? "yellow" : "green")}`);
|
|
10804
|
+
console.log("");
|
|
10805
|
+
console.log(colorize(" \u2500\u2500\u2500 \u5F00\u5934\u7C7B\u578B\u5206\u5E03 \u2500\u2500\u2500", "cyan"));
|
|
10806
|
+
const typeNames = {
|
|
10807
|
+
"single-sensory": "\u5355\u5B57\u611F\u5B98",
|
|
10808
|
+
"dialogue": "\u5BF9\u8BDD",
|
|
10809
|
+
"action": "\u52A8\u4F5C",
|
|
10810
|
+
"description": "\u63CF\u5199",
|
|
10811
|
+
"internal-thought": "\u5185\u5FC3"
|
|
10812
|
+
};
|
|
10813
|
+
for (const [type, count] of Object.entries(result.stats.openingTypeCounts)) {
|
|
10814
|
+
const name = typeNames[type] || type;
|
|
10815
|
+
const warn = type === "single-sensory" && count >= 3;
|
|
10816
|
+
console.log(` ${name.padEnd(6)}: ${colorize(String(count), warn ? "yellow" : "cyan")}\u7AE0 ${warn ? colorize("\u2190 \u504F\u591A\uFF0C\u5EFA\u8BAE\u53D8\u5316", "yellow") : ""}`);
|
|
10817
|
+
}
|
|
10818
|
+
console.log("");
|
|
10819
|
+
console.log(colorize(" \u2500\u2500\u2500 \u7ED3\u5C3E\u7C7B\u578B\u5206\u5E03 \u2500\u2500\u2500", "cyan"));
|
|
10820
|
+
const endNames = {
|
|
10821
|
+
"reveal": "\u5426\u5B9A\u63ED\u793A",
|
|
10822
|
+
"cliffhanger": "\u60AC\u5FF5\u63D0\u95EE",
|
|
10823
|
+
"dialogue": "\u5BF9\u8BDD\u6536\u5C3E",
|
|
10824
|
+
"action": "\u52A8\u4F5C\u6536\u5C3E",
|
|
10825
|
+
"emotion": "\u60C5\u7EEA\u77ED\u53E5"
|
|
10826
|
+
};
|
|
10827
|
+
for (const [type, count] of Object.entries(result.stats.endingTypeCounts)) {
|
|
10828
|
+
const name = endNames[type] || type;
|
|
10829
|
+
const warn = type === "reveal" && count >= 3;
|
|
10830
|
+
console.log(` ${name.padEnd(6)}: ${colorize(String(count), warn ? "yellow" : "cyan")}\u7AE0 ${warn ? colorize("\u2190 \u5957\u8DEF\u5316\u98CE\u9669", "yellow") : ""}`);
|
|
10831
|
+
}
|
|
10832
|
+
console.log("");
|
|
10833
|
+
printBookIssues(result.issues);
|
|
10834
|
+
}
|
|
10394
10835
|
var args = process.argv.slice(2);
|
|
10395
10836
|
if (args.length === 0 || args.includes("-h") || args.includes("--help")) {
|
|
10396
10837
|
printHelp();
|
|
@@ -10429,6 +10870,20 @@ if (cleanArgs[0] === "-" || cleanArgs[0] === "--stdin") {
|
|
|
10429
10870
|
}
|
|
10430
10871
|
const text = fs.readFileSync(resolvedPath, "utf-8");
|
|
10431
10872
|
runCheck(text, filePath, outputJson);
|
|
10873
|
+
} else if (cleanArgs[0] === "book") {
|
|
10874
|
+
const filePath = cleanArgs[1];
|
|
10875
|
+
if (!filePath) {
|
|
10876
|
+
console.error(colorize("\u9519\u8BEF: \u8BF7\u6307\u5B9A\u8981\u68C0\u6D4B\u7684\u6587\u4EF6\u8DEF\u5F84\uFF08\u652F\u6301\u542B\u591A\u7AE0\u7684\u5168\u4E66\u6587\u4EF6\uFF09", "red"));
|
|
10877
|
+
console.log("\u7528\u6CD5: gwe book <novel.txt>");
|
|
10878
|
+
process.exit(1);
|
|
10879
|
+
}
|
|
10880
|
+
const resolvedPath = path.resolve(process.cwd(), filePath);
|
|
10881
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
10882
|
+
console.error(colorize(`\u9519\u8BEF: \u6587\u4EF6\u4E0D\u5B58\u5728: ${resolvedPath}`, "red"));
|
|
10883
|
+
process.exit(1);
|
|
10884
|
+
}
|
|
10885
|
+
const text = fs.readFileSync(resolvedPath, "utf-8");
|
|
10886
|
+
runBookCheck(text, filePath);
|
|
10432
10887
|
} else {
|
|
10433
10888
|
const filePath = cleanArgs[0];
|
|
10434
10889
|
const resolvedPath = path.resolve(process.cwd(), filePath);
|