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 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.2.0";
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\u4E00\u4E2A\u7F51\u6587\u7AE0\u8282\u6587\u4EF6
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);