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/index.d.ts CHANGED
@@ -844,4 +844,166 @@ declare function validateSelectionsComplete(selections: Partial<Record<NodeId, O
844
844
  */
845
845
  declare function _resetRegistry(): void;
846
846
 
847
- export { type AnchorDetectionResult, type BookMeta, type BuildPromptOptions, type BuildUserMessageOptions, type CharacterRelationship, type ChatMessage, type ChatRole, type CheckResult, type ConflictInfo, type CustomRule, DEFAULT_RADAR_WEIGHTS, DEFAULT_THRESHOLDS, type DependencyInfo, type EngineConfig, type EngineEvent, type EngineEventListener, type EngineEventType, type FillerDetectionResult, type Chapter as GWEChapter, type Character as GWECharacter, GWEEngine, type Setting as GWESetting, type Subplot as GWESubplot, type Volume as GWEVolume, type LLMProvider, type LLMRequest, type LLMUsage, type LoadResult, type MergeOptions, type MergedConfig, type MergedVocabulary, MockProvider, type NodeCatalog, type NodeCategory, type NodeDefinition, type NodeId, type NodeOptionKB, type OpenAICompatibleConfig, OpenAICompatibleProvider, type OptionId, PRESET_PROVIDERS, type Preset, type PresetProvider, type RadarInput, type RadarScores, type RadarWeights, type RuleViolation, type StreamCallbacks, type TaskType, type TextSelection, type TextStats, type Thresholds, type UserOverrides, type ValidationResult, type WriteTask, type WritingContext, _resetRegistry, buildSystemPrompt, buildUserMessage, calculateAnchorDensity, calculateFillerDensity, calculateMaxAnchorGap, calculateRadar, calculateScore, calculateWeightedScore, canSelectOption, check, cloneMergedConfig, countSensoryMentions, createEngineWithKB, createProvider, detectAnchors, detectFillers, formatValidationResult, getAllNodes, getDefaultSelections, getDefaultSelectionsFull, getNode, getNodeOption, getNodesByCategory, getRegisteredOptionCount, loadAllKB, loadKBFromJSON, mergeConfig, registerNode, registerNodeOption, validate, validateSelectionsComplete };
847
+ /**
848
+ * BookContext - 全书上下文追踪器
849
+ *
850
+ * 跟踪跨章节信息,解决单章检测无法发现的问题:
851
+ * - 开头/结尾句式重复套路化
852
+ * - 设定规则违反
853
+ * - 章节衔接断裂
854
+ * - 伏笔回收追踪
855
+ */
856
+ interface ChapterSnapshot {
857
+ index: number;
858
+ title: string;
859
+ /** 首句 */
860
+ firstSentence: string;
861
+ /** 首句结构特征 */
862
+ openingPattern: OpeningPattern;
863
+ /** 末3句 */
864
+ lastSentences: string[];
865
+ /** 结尾结构特征 */
866
+ endingPattern: EndingPattern;
867
+ /** 本章出场人物及其最后状态 */
868
+ characterStates: Map<string, CharacterState>;
869
+ /** 本章建立/引用的设定规则 */
870
+ settingRules: SettingRule[];
871
+ /** 本章埋下的伏笔 */
872
+ foreshadowing: Foreshadow[];
873
+ /** 本章回收的伏笔(关键词匹配) */
874
+ resolvedForeshadow: string[];
875
+ /** 本章结尾地点/场景 */
876
+ closingScene: string;
877
+ /** 字数 */
878
+ charCount: number;
879
+ }
880
+ interface OpeningPattern {
881
+ /** 首句长度(字数) */
882
+ length: number;
883
+ /** 首句类型:single-sensory(单字感官如"疼。""麻。")/ dialogue / action / description / internal-thought */
884
+ type: 'single-sensory' | 'dialogue' | 'action' | 'description' | 'internal-thought';
885
+ /** 首句核心词 */
886
+ keyword: string;
887
+ /** 结构签名:用于跨章比对 */
888
+ signature: string;
889
+ }
890
+ interface EndingPattern {
891
+ /** 结尾类型:reveal(揭示)/ cliffhanger(悬念)/ dialogue / action / emotion */
892
+ type: 'reveal' | 'cliffhanger' | 'dialogue' | 'action' | 'emotion';
893
+ /** 是否使用了否定→肯定断句模式("不是X。是Y。") */
894
+ usesNegationReveal: boolean;
895
+ /** 结尾句数量(断句堆叠数) */
896
+ fragmentCount: number;
897
+ /** 最后一句字数 */
898
+ lastSentenceLength: number;
899
+ /** 结构签名 */
900
+ signature: string;
901
+ }
902
+ interface CharacterState {
903
+ name: string;
904
+ /** 最后出现位置描述 */
905
+ lastLocation: string;
906
+ /** 最后动作 */
907
+ lastAction: string;
908
+ /** 本章出场章节号 */
909
+ lastChapter: number;
910
+ }
911
+ interface SettingRule {
912
+ /** 规则描述 */
913
+ rule: string;
914
+ /** 规则类型:absolute(永远/从不)/ conditional(在X条件下) */
915
+ type: 'absolute' | 'conditional';
916
+ /** 规则关键词,用于后续检测违反 */
917
+ keywords: string[];
918
+ /** 被哪一章建立 */
919
+ establishedIn: number;
920
+ }
921
+ interface Foreshadow {
922
+ /** 伏笔关键词 */
923
+ keyword: string;
924
+ /** 伏笔描述 */
925
+ description: string;
926
+ /** 埋设章节 */
927
+ plantedIn: number;
928
+ /** 重要程度 1-3 */
929
+ importance: 1 | 2 | 3;
930
+ /** 是否已回收 */
931
+ resolved: boolean;
932
+ }
933
+ interface BookIssue {
934
+ level: 'error' | 'warning' | 'info';
935
+ type: 'repetitive-opening' | 'repetitive-ending' | 'setting-violation' | 'continuity-break' | 'unresolved-foreshadow' | 'stale-thread';
936
+ message: string;
937
+ chapterIndex?: number;
938
+ details?: string;
939
+ }
940
+ declare function detectOpeningPattern(firstSentence: string): OpeningPattern;
941
+ declare function detectEndingPattern(lastSentences: string[]): EndingPattern;
942
+ declare function extractSettingRules(text: string, chapterIndex: number): SettingRule[];
943
+ declare function extractForeshadowing(text: string, chapterIndex: number): Foreshadow[];
944
+ declare class BookContext {
945
+ private chapters;
946
+ private allSettingRules;
947
+ private allForeshadowing;
948
+ private globalCharacterStates;
949
+ /** 添加一个章节的快照,返回检测到的全书级别问题 */
950
+ addChapter(snapshot: ChapterSnapshot): BookIssue[];
951
+ /** 检测最近N章开头模式是否重复 */
952
+ private checkOpeningRepetition;
953
+ /** 检测最近N章结尾模式是否重复 */
954
+ private checkEndingRepetition;
955
+ /** 检测本章是否违反之前建立的设定 */
956
+ private checkSettingViolations;
957
+ /** 检测与上一章的衔接是否断裂 */
958
+ private checkContinuity;
959
+ /** 追踪伏笔,标记长期未回收的伏笔 */
960
+ private checkForeshadowing;
961
+ getChapterCount(): number;
962
+ getStats(): {
963
+ chapters: number;
964
+ totalForeshadowing: number;
965
+ unresolvedForeshadowing: number;
966
+ settingRules: number;
967
+ characters: number;
968
+ };
969
+ }
970
+
971
+ /**
972
+ * book-checker.ts - 全书级别检测器
973
+ *
974
+ * 使用 BookContext 对多章文本进行连续性/一致性/套路化检测
975
+ */
976
+
977
+ interface BookCheckResult {
978
+ issues: BookIssue[];
979
+ chapters: ChapterSnapshot[];
980
+ stats: {
981
+ totalChapters: number;
982
+ totalChars: number;
983
+ openingTypeCounts: Record<string, number>;
984
+ endingTypeCounts: Record<string, number>;
985
+ totalForeshadowing: number;
986
+ unresolvedForeshadowing: number;
987
+ repetitiveOpenings: number;
988
+ repetitiveEndings: number;
989
+ settingViolations: number;
990
+ continuityBreaks: number;
991
+ };
992
+ }
993
+ /** 将文本拆分为章节 */
994
+ declare function splitChapters(text: string): {
995
+ title: string;
996
+ content: string;
997
+ index: number;
998
+ }[];
999
+ /** 从单章文本提取ChapterSnapshot */
1000
+ declare function extractChapterSnapshot(text: string, index: number, title?: string): ChapterSnapshot;
1001
+ /** 对全书文本进行检测 */
1002
+ declare function checkBook(fullText: string): BookCheckResult;
1003
+ /** 对章节文件列表进行检测(每章一个文件) */
1004
+ declare function checkChapterFiles(files: {
1005
+ name: string;
1006
+ content: string;
1007
+ }[]): BookCheckResult;
1008
+
1009
+ export { type AnchorDetectionResult, type BookCheckResult, BookContext, type BookIssue, type BookMeta, type BuildPromptOptions, type BuildUserMessageOptions, type ChapterSnapshot, type CharacterRelationship, type CharacterState, type ChatMessage, type ChatRole, type CheckResult, type ConflictInfo, type CustomRule, DEFAULT_RADAR_WEIGHTS, DEFAULT_THRESHOLDS, type DependencyInfo, type EndingPattern, type EngineConfig, type EngineEvent, type EngineEventListener, type EngineEventType, type FillerDetectionResult, type Foreshadow, type Chapter as GWEChapter, type Character as GWECharacter, GWEEngine, type Setting as GWESetting, type Subplot as GWESubplot, type Volume as GWEVolume, type LLMProvider, type LLMRequest, type LLMUsage, type LoadResult, type MergeOptions, type MergedConfig, type MergedVocabulary, MockProvider, type NodeCatalog, type NodeCategory, type NodeDefinition, type NodeId, type NodeOptionKB, type OpenAICompatibleConfig, OpenAICompatibleProvider, type OpeningPattern, type OptionId, PRESET_PROVIDERS, type Preset, type PresetProvider, type RadarInput, type RadarScores, type RadarWeights, type RuleViolation, type SettingRule, type StreamCallbacks, type TaskType, type TextSelection, type TextStats, type Thresholds, type UserOverrides, type ValidationResult, type WriteTask, type WritingContext, _resetRegistry, buildSystemPrompt, buildUserMessage, calculateAnchorDensity, calculateFillerDensity, calculateMaxAnchorGap, calculateRadar, calculateScore, calculateWeightedScore, canSelectOption, check, checkBook, checkChapterFiles, cloneMergedConfig, countSensoryMentions, createEngineWithKB, createProvider, detectAnchors, detectEndingPattern, detectFillers, detectOpeningPattern, extractChapterSnapshot, extractForeshadowing, extractSettingRules, formatValidationResult, getAllNodes, getDefaultSelections, getDefaultSelectionsFull, getNode, getNodeOption, getNodesByCategory, getRegisteredOptionCount, loadAllKB, loadKBFromJSON, mergeConfig, registerNode, registerNodeOption, splitChapters, validate, validateSelectionsComplete };
package/dist/index.js CHANGED
@@ -10526,7 +10526,426 @@ function createEngineWithKB(provider) {
10526
10526
  const result = loadAllKB(engine);
10527
10527
  return { engine, result };
10528
10528
  }
10529
+
10530
+ // src/book-context.ts
10531
+ var SENSORY_SINGLE = /^(疼|麻|冷|热|烫|酸|胀|痒|涩|苦|甜|咸|腥|臭|香|黑|亮|静|响|湿|干|硬|软|滑|糙|重|轻)。?$/;
10532
+ var DIALOGUE_OPEN = /^[""「"]/;
10533
+ var NEGATION_REVEAL = /不是[^。?!]{1,8}[。?!]\s*是/g;
10534
+ function detectOpeningPattern(firstSentence) {
10535
+ const trimmed = firstSentence.trim();
10536
+ const len = trimmed.length;
10537
+ let type = "description";
10538
+ let keyword = trimmed.slice(0, 4);
10539
+ let signature = "";
10540
+ if (SENSORY_SINGLE.test(trimmed) || len <= 3 && /[疼麻冷热烫酸胀痛痒涩]/.test(trimmed)) {
10541
+ type = "single-sensory";
10542
+ keyword = trimmed.replace(/[。!?]/g, "");
10543
+ signature = `sensory:${keyword}`;
10544
+ } else if (DIALOGUE_OPEN.test(trimmed)) {
10545
+ type = "dialogue";
10546
+ signature = "dialogue";
10547
+ } else if (/^(他|她|我|[\u4e00-\u9fa5]{2,3})(把|将|用|举|拿|握|伸|抬|踢|踩|抓|放|推|拉|扯|拽|蹲|站|跳|跑|走|转身|回头|低头|抬头)/.test(trimmed)) {
10548
+ type = "action";
10549
+ signature = `action:${trimmed.slice(0, 2)}`;
10550
+ } else if (/^(我|他|她)(想|觉得|意识到|知道|明白|想起|记得|感觉|以为|怀疑)/.test(trimmed)) {
10551
+ type = "internal-thought";
10552
+ signature = "internal";
10553
+ } else {
10554
+ type = "description";
10555
+ signature = `desc:${len}`;
10556
+ }
10557
+ return { length: len, type, keyword, signature };
10558
+ }
10559
+ function detectEndingPattern(lastSentences) {
10560
+ const fullEnding = lastSentences.join("");
10561
+ const lastSent = lastSentences[lastSentences.length - 1]?.trim() || "";
10562
+ const negationMatches = fullEnding.match(NEGATION_REVEAL);
10563
+ const usesNegationReveal = negationMatches !== null && negationMatches.length >= 1;
10564
+ const fragmentCount = lastSentences.filter((s) => s.trim().length <= 8).length;
10565
+ let type = "action";
10566
+ if (usesNegationReveal && /(是|原来|其实)/.test(fullEnding.slice(-20))) {
10567
+ type = "reveal";
10568
+ } else if (/[??]|吗|呢|难道|会不会|是不是/.test(lastSent)) {
10569
+ type = "cliffhanger";
10570
+ } else if (DIALOGUE_OPEN.test(lastSent)) {
10571
+ type = "dialogue";
10572
+ } else if (usesNegationReveal) {
10573
+ type = "reveal";
10574
+ } else if (fragmentCount >= 2 && lastSent.length <= 6) {
10575
+ type = "emotion";
10576
+ } else {
10577
+ type = "action";
10578
+ }
10579
+ const signature = `${type}:${usesNegationReveal ? "neg" : "normal"}:${fragmentCount}`;
10580
+ return {
10581
+ type,
10582
+ usesNegationReveal,
10583
+ fragmentCount,
10584
+ lastSentenceLength: lastSent.length,
10585
+ signature
10586
+ };
10587
+ }
10588
+ function extractSettingRules(text, chapterIndex) {
10589
+ const rules = [];
10590
+ const absolutePatterns = [
10591
+ /([^,。?!\n]{2,15})永远(?:是|不会|不能|不可能)([^,。?!\n]{2,15})/g,
10592
+ /([^,。?!\n]{2,15})从来(?:不|没|没有)([^,。?!\n]{2,15})/g,
10593
+ /([^,。?!\n]{2,15})(?:绝?对|一定|必须)是([^,。?!\n]{2,15})/g,
10594
+ /([^,。?!\n]{2,8})只(?:能|会|有)([^,。?!\n]{2,15})/g
10595
+ ];
10596
+ for (const pattern of absolutePatterns) {
10597
+ let match;
10598
+ while ((match = pattern.exec(text)) !== null) {
10599
+ rules.push({
10600
+ rule: match[0],
10601
+ type: "absolute",
10602
+ keywords: [match[1].slice(-4), match[2].slice(0, 4)],
10603
+ establishedIn: chapterIndex
10604
+ });
10605
+ }
10606
+ }
10607
+ return rules;
10608
+ }
10609
+ function extractForeshadowing(text, chapterIndex) {
10610
+ const foreshadows = [];
10611
+ const plantPatterns = [
10612
+ { re: /([^,。?!\n]{0,10}(?:想起|记得|回忆起|意识到|感觉到|注意到)[^,。?!\n]{2,25})/g, importance: 2 },
10613
+ { re: /([^,。?!\n]{0,10}(?:说过|警告过|提醒过|告诉过他)[^,。?!\n]{2,25})/g, importance: 3 },
10614
+ { re: /(不对劲|有问题|不对|奇怪|异常|反常|不寻常)/g, importance: 2 }
10615
+ ];
10616
+ const seen = /* @__PURE__ */ new Set();
10617
+ for (const { re, importance } of plantPatterns) {
10618
+ let match;
10619
+ while ((match = re.exec(text)) !== null) {
10620
+ const desc = match[1] || match[0];
10621
+ const key = desc.slice(0, 10);
10622
+ if (seen.has(key)) continue;
10623
+ seen.add(key);
10624
+ foreshadows.push({
10625
+ keyword: key,
10626
+ description: desc.slice(0, 40),
10627
+ plantedIn: chapterIndex,
10628
+ importance,
10629
+ resolved: false
10630
+ });
10631
+ }
10632
+ }
10633
+ return foreshadows;
10634
+ }
10635
+ var BookContext = class {
10636
+ chapters = [];
10637
+ allSettingRules = [];
10638
+ allForeshadowing = [];
10639
+ globalCharacterStates = /* @__PURE__ */ new Map();
10640
+ /** 添加一个章节的快照,返回检测到的全书级别问题 */
10641
+ addChapter(snapshot) {
10642
+ const issues = [];
10643
+ const openingIssues = this.checkOpeningRepetition(snapshot);
10644
+ issues.push(...openingIssues);
10645
+ const endingIssues = this.checkEndingRepetition(snapshot);
10646
+ issues.push(...endingIssues);
10647
+ const settingIssues = this.checkSettingViolations(snapshot);
10648
+ issues.push(...settingIssues);
10649
+ const continuityIssues = this.checkContinuity(snapshot);
10650
+ issues.push(...continuityIssues);
10651
+ const foreshadowIssues = this.checkForeshadowing(snapshot);
10652
+ issues.push(...foreshadowIssues);
10653
+ this.chapters.push(snapshot);
10654
+ for (const rule of snapshot.settingRules) {
10655
+ this.allSettingRules.push(rule);
10656
+ }
10657
+ for (const fs of snapshot.foreshadowing) {
10658
+ this.allForeshadowing.push(fs);
10659
+ }
10660
+ for (const [name, state] of snapshot.characterStates) {
10661
+ this.globalCharacterStates.set(name, state);
10662
+ }
10663
+ return issues;
10664
+ }
10665
+ /** 检测最近N章开头模式是否重复 */
10666
+ checkOpeningRepetition(snapshot) {
10667
+ const issues = [];
10668
+ const recentN = 5;
10669
+ const recent = this.chapters.slice(-recentN);
10670
+ const sameTypeCount = recent.filter((c) => c.openingPattern.type === snapshot.openingPattern.type).length;
10671
+ const sameSensoryCount = recent.filter((c) => c.openingPattern.type === "single-sensory").length;
10672
+ if (sameSensoryCount >= 1 && snapshot.openingPattern.type === "single-sensory") {
10673
+ issues.push({
10674
+ level: sameSensoryCount >= 2 ? "warning" : "info",
10675
+ type: "repetitive-opening",
10676
+ 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`,
10677
+ chapterIndex: snapshot.index
10678
+ });
10679
+ }
10680
+ if (sameTypeCount >= 2 && snapshot.openingPattern.type !== "single-sensory") {
10681
+ issues.push({
10682
+ level: "info",
10683
+ type: "repetitive-opening",
10684
+ message: `\u8FDE\u7EED${sameTypeCount + 1}\u7AE0\u4F7F\u7528${snapshot.openingPattern.type}\u7C7B\u578B\u5F00\u5934\uFF0C\u5EFA\u8BAE\u53D8\u5316\u8282\u594F\u3002`,
10685
+ chapterIndex: snapshot.index
10686
+ });
10687
+ }
10688
+ return issues;
10689
+ }
10690
+ /** 检测最近N章结尾模式是否重复 */
10691
+ checkEndingRepetition(snapshot) {
10692
+ const issues = [];
10693
+ const recentN = 5;
10694
+ const recent = this.chapters.slice(-recentN);
10695
+ const sameEndingCount = recent.filter((c) => c.endingPattern.signature === snapshot.endingPattern.signature).length;
10696
+ const sameNegationCount = recent.filter((c) => c.endingPattern.usesNegationReveal).length;
10697
+ if (sameNegationCount >= 1 && snapshot.endingPattern.usesNegationReveal) {
10698
+ issues.push({
10699
+ level: sameNegationCount >= 2 ? "warning" : "info",
10700
+ type: "repetitive-ending",
10701
+ 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`,
10702
+ chapterIndex: snapshot.index
10703
+ });
10704
+ }
10705
+ if (sameEndingCount >= 2 && snapshot.endingPattern.fragmentCount >= 3) {
10706
+ issues.push({
10707
+ level: "info",
10708
+ type: "repetitive-ending",
10709
+ 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`,
10710
+ chapterIndex: snapshot.index
10711
+ });
10712
+ }
10713
+ return issues;
10714
+ }
10715
+ /** 检测本章是否违反之前建立的设定 */
10716
+ checkSettingViolations(snapshot) {
10717
+ const issues = [];
10718
+ for (const rule of this.allSettingRules) {
10719
+ const { keywords, rule: ruleText, establishedIn } = rule;
10720
+ if (keywords.length < 2) continue;
10721
+ const isNegative = /不|没|从未|永不/.test(ruleText);
10722
+ if (!isNegative) continue;
10723
+ const [subject, predicate] = keywords;
10724
+ const violationRegex = new RegExp(`${subject}[^\uFF0C\u3002\uFF1F\uFF01]{0,15}${predicate.replace(/不|没/g, "")}`);
10725
+ }
10726
+ return issues;
10727
+ }
10728
+ /** 检测与上一章的衔接是否断裂 */
10729
+ checkContinuity(snapshot) {
10730
+ const issues = [];
10731
+ if (this.chapters.length === 0) return issues;
10732
+ const prev = this.chapters[this.chapters.length - 1];
10733
+ const prevScene = prev.closingScene;
10734
+ const firstSent = snapshot.firstSentence;
10735
+ const openContext = firstSent.slice(0, 50);
10736
+ const hasCharacterLink = [...snapshot.characterStates.keys()].some(
10737
+ (name) => prev.characterStates.has(name)
10738
+ );
10739
+ if (!hasCharacterLink && prev.characterStates.size > 0 && snapshot.characterStates.size > 0) {
10740
+ const prevNames = [...prev.characterStates.keys()].slice(0, 3).join("\u3001");
10741
+ issues.push({
10742
+ level: "warning",
10743
+ type: "continuity-break",
10744
+ 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`,
10745
+ chapterIndex: snapshot.index
10746
+ });
10747
+ }
10748
+ return issues;
10749
+ }
10750
+ /** 追踪伏笔,标记长期未回收的伏笔 */
10751
+ checkForeshadowing(snapshot) {
10752
+ const issues = [];
10753
+ const currentText = snapshot.lastSentences.join("") + snapshot.firstSentence;
10754
+ for (const fs of this.allForeshadowing) {
10755
+ if (fs.resolved) continue;
10756
+ if (currentText.includes(fs.keyword.slice(0, 3))) {
10757
+ fs.resolved = true;
10758
+ }
10759
+ }
10760
+ for (const fs of this.allForeshadowing) {
10761
+ if (fs.resolved) continue;
10762
+ const gap = snapshot.index - fs.plantedIn;
10763
+ if (fs.importance >= 3 && gap >= 5) {
10764
+ issues.push({
10765
+ level: "warning",
10766
+ type: "unresolved-foreshadow",
10767
+ message: `\u91CD\u8981\u4F0F\u7B14"${fs.keyword}\u2026\u2026"\u5728\u7B2C${fs.plantedIn + 1}\u7AE0\u57CB\u8BBE\uFF0C\u5DF2\u8FC7${gap}\u7AE0\u672A\u56DE\u6536\u3002`,
10768
+ chapterIndex: snapshot.index,
10769
+ details: fs.description
10770
+ });
10771
+ } else if (fs.importance >= 2 && gap >= 10) {
10772
+ issues.push({
10773
+ level: "info",
10774
+ type: "stale-thread",
10775
+ message: `\u4F0F\u7B14"${fs.keyword}\u2026\u2026"\u5DF2\u8FC7${gap}\u7AE0\u672A\u56DE\u6536\uFF0C\u53EF\u80FD\u5DF2\u88AB\u9057\u5FD8\u3002`,
10776
+ chapterIndex: snapshot.index
10777
+ });
10778
+ }
10779
+ }
10780
+ return issues;
10781
+ }
10782
+ getChapterCount() {
10783
+ return this.chapters.length;
10784
+ }
10785
+ getStats() {
10786
+ const unresolved = this.allForeshadowing.filter((f) => !f.resolved).length;
10787
+ const total = this.allForeshadowing.length;
10788
+ return {
10789
+ chapters: this.chapters.length,
10790
+ totalForeshadowing: total,
10791
+ unresolvedForeshadowing: unresolved,
10792
+ settingRules: this.allSettingRules.length,
10793
+ characters: this.globalCharacterStates.size
10794
+ };
10795
+ }
10796
+ };
10797
+
10798
+ // src/book-checker.ts
10799
+ var CHAPTER_HEADER = /第[一二三四五六七八九十百千万零\d]+[章节回卷部][\s\n]*/g;
10800
+ function splitChapters(text) {
10801
+ const matches = [...text.matchAll(CHAPTER_HEADER)];
10802
+ if (matches.length === 0) {
10803
+ return [{ title: "\u5168\u6587", content: text.trim(), index: 0 }];
10804
+ }
10805
+ const chapters = [];
10806
+ for (let i = 0; i < matches.length; i++) {
10807
+ const match = matches[i];
10808
+ const titleStart = match.index ?? 0;
10809
+ const contentStart = titleStart + match[0].length;
10810
+ const contentEnd = i + 1 < matches.length ? matches[i + 1].index ?? text.length : text.length;
10811
+ const title = match[0].trim();
10812
+ const content = text.slice(contentStart, contentEnd).trim();
10813
+ if (content.length > 30) {
10814
+ chapters.push({ title, content, index: chapters.length });
10815
+ }
10816
+ }
10817
+ return chapters.length > 0 ? chapters : [{ title: "\u5168\u6587", content: text.trim(), index: 0 }];
10818
+ }
10819
+ function extractChapterSnapshot(text, index, title = `\u7B2C${index + 1}\u7AE0`) {
10820
+ const sentences = text.replace(/\r\n/g, "\n").split(/(?<=[。!?…])\s*\n?|(?<=[。!?])\s+/).map((s) => s.trim()).filter((s) => s.length > 0);
10821
+ const firstSentence = sentences[0] || text.slice(0, 20);
10822
+ const lastSentences = sentences.slice(-5);
10823
+ const nameRegex = /[\u4e00-\u9fa5]{2,3}(?=说|道|问|喊|叫|笑|怒|叹|想|看|听|走|跑|站|蹲|转身|回头|点头|摇头|皱眉|咬|握|拿|举|伸|抬)/g;
10824
+ const nameCounts = /* @__PURE__ */ new Map();
10825
+ let m;
10826
+ while ((m = nameRegex.exec(text)) !== null) {
10827
+ const name = m[0];
10828
+ if (/^(自己|大家|众人|对方|这个|那个|什么|怎么|没有|不是|可以|已经|突然|然后|可是|但是|如果|因为|所以|虽然|只是|就是|还是|还有|有些|一点|一下|一声|一眼|脸上|心里|眼中|时候|地方|东西|声音|样子|问题|事情)/.test(name)) continue;
10829
+ nameCounts.set(name, (nameCounts.get(name) || 0) + 1);
10830
+ }
10831
+ const characterStates = /* @__PURE__ */ new Map();
10832
+ for (const [name, count] of nameCounts) {
10833
+ if (count >= 2) {
10834
+ const lastIdx = text.lastIndexOf(name);
10835
+ const context = text.slice(Math.max(0, lastIdx - 10), Math.min(text.length, lastIdx + 30));
10836
+ characterStates.set(name, {
10837
+ name,
10838
+ lastLocation: "",
10839
+ lastAction: context.slice(0, 30),
10840
+ lastChapter: index
10841
+ });
10842
+ }
10843
+ }
10844
+ const endingText = text.slice(-200);
10845
+ const closingScene = endingText.slice(0, 50);
10846
+ const settingRules = extractSettingRules(text, index);
10847
+ const foreshadowing = extractForeshadowing(text, index);
10848
+ return {
10849
+ index,
10850
+ title,
10851
+ firstSentence,
10852
+ openingPattern: detectOpeningPattern(firstSentence),
10853
+ lastSentences,
10854
+ endingPattern: detectEndingPattern(lastSentences),
10855
+ characterStates,
10856
+ settingRules,
10857
+ foreshadowing,
10858
+ resolvedForeshadow: [],
10859
+ closingScene,
10860
+ charCount: text.length
10861
+ };
10862
+ }
10863
+ function checkBook(fullText) {
10864
+ const chapters = splitChapters(fullText);
10865
+ const ctx = new BookContext();
10866
+ const allIssues = [];
10867
+ const snapshots = [];
10868
+ let totalChars = 0;
10869
+ const openingCounts = {};
10870
+ const endingCounts = {};
10871
+ let repOpen = 0, repEnd = 0, setVio = 0, contBrk = 0;
10872
+ for (const { title, content, index } of chapters) {
10873
+ const snap = extractChapterSnapshot(content, index, title);
10874
+ snapshots.push(snap);
10875
+ totalChars += snap.charCount;
10876
+ openingCounts[snap.openingPattern.type] = (openingCounts[snap.openingPattern.type] || 0) + 1;
10877
+ endingCounts[snap.endingPattern.type] = (endingCounts[snap.endingPattern.type] || 0) + 1;
10878
+ const issues = ctx.addChapter(snap);
10879
+ for (const issue of issues) {
10880
+ allIssues.push(issue);
10881
+ if (issue.type === "repetitive-opening") repOpen++;
10882
+ if (issue.type === "repetitive-ending") repEnd++;
10883
+ if (issue.type === "setting-violation") setVio++;
10884
+ if (issue.type === "continuity-break") contBrk++;
10885
+ }
10886
+ }
10887
+ const stats = ctx.getStats();
10888
+ return {
10889
+ issues: allIssues,
10890
+ chapters: snapshots,
10891
+ stats: {
10892
+ totalChapters: chapters.length,
10893
+ totalChars,
10894
+ openingTypeCounts: openingCounts,
10895
+ endingTypeCounts: endingCounts,
10896
+ totalForeshadowing: stats.totalForeshadowing,
10897
+ unresolvedForeshadowing: stats.unresolvedForeshadowing,
10898
+ repetitiveOpenings: repOpen,
10899
+ repetitiveEndings: repEnd,
10900
+ settingViolations: setVio,
10901
+ continuityBreaks: contBrk
10902
+ }
10903
+ };
10904
+ }
10905
+ function checkChapterFiles(files) {
10906
+ const ctx = new BookContext();
10907
+ const allIssues = [];
10908
+ const snapshots = [];
10909
+ let totalChars = 0;
10910
+ const openingCounts = {};
10911
+ const endingCounts = {};
10912
+ let repOpen = 0, repEnd = 0, setVio = 0, contBrk = 0;
10913
+ for (let i = 0; i < files.length; i++) {
10914
+ const { name, content } = files[i];
10915
+ const snap = extractChapterSnapshot(content, i, name);
10916
+ snapshots.push(snap);
10917
+ totalChars += snap.charCount;
10918
+ openingCounts[snap.openingPattern.type] = (openingCounts[snap.openingPattern.type] || 0) + 1;
10919
+ endingCounts[snap.endingPattern.type] = (endingCounts[snap.endingPattern.type] || 0) + 1;
10920
+ const issues = ctx.addChapter(snap);
10921
+ for (const issue of issues) {
10922
+ allIssues.push(issue);
10923
+ if (issue.type === "repetitive-opening") repOpen++;
10924
+ if (issue.type === "repetitive-ending") repEnd++;
10925
+ if (issue.type === "setting-violation") setVio++;
10926
+ if (issue.type === "continuity-break") contBrk++;
10927
+ }
10928
+ }
10929
+ const stats = ctx.getStats();
10930
+ return {
10931
+ issues: allIssues,
10932
+ chapters: snapshots,
10933
+ stats: {
10934
+ totalChapters: files.length,
10935
+ totalChars,
10936
+ openingTypeCounts: openingCounts,
10937
+ endingTypeCounts: endingCounts,
10938
+ totalForeshadowing: stats.totalForeshadowing,
10939
+ unresolvedForeshadowing: stats.unresolvedForeshadowing,
10940
+ repetitiveOpenings: repOpen,
10941
+ repetitiveEndings: repEnd,
10942
+ settingViolations: setVio,
10943
+ continuityBreaks: contBrk
10944
+ }
10945
+ };
10946
+ }
10529
10947
  export {
10948
+ BookContext,
10530
10949
  DEFAULT_RADAR_WEIGHTS,
10531
10950
  DEFAULT_THRESHOLDS,
10532
10951
  GWEEngine,
@@ -10544,12 +10963,19 @@ export {
10544
10963
  calculateWeightedScore,
10545
10964
  canSelectOption,
10546
10965
  check,
10966
+ checkBook,
10967
+ checkChapterFiles,
10547
10968
  cloneMergedConfig,
10548
10969
  countSensoryMentions,
10549
10970
  createEngineWithKB,
10550
10971
  createProvider,
10551
10972
  detectAnchors,
10973
+ detectEndingPattern,
10552
10974
  detectFillers,
10975
+ detectOpeningPattern,
10976
+ extractChapterSnapshot,
10977
+ extractForeshadowing,
10978
+ extractSettingRules,
10553
10979
  formatValidationResult,
10554
10980
  getAllNodes,
10555
10981
  getDefaultSelections,
@@ -10563,6 +10989,7 @@ export {
10563
10989
  mergeConfig,
10564
10990
  registerNode,
10565
10991
  registerNodeOption,
10992
+ splitChapters,
10566
10993
  validate,
10567
10994
  validateSelectionsComplete
10568
10995
  };