Jelajahi Sumber

feat: add cross-chapter repetition detection

detectCrossChapterRepetition checks current chapter against recent 5
chapters for repeated phrases (6-char ngrams for zh, 3-word for en).
Warns when 3+ repeated phrases also appear in recent chapters.
Addresses "liora stared at" x8 chapters pattern in evaluation.
Ma 7 bulan lalu
induk
melakukan
ae2f31d3a1

+ 70 - 1
packages/core/src/agents/post-write-validator.ts

@@ -52,11 +52,12 @@ export function validatePostWrite(
   content: string,
   content: string,
   genreProfile: GenreProfile,
   genreProfile: GenreProfile,
   bookRules: BookRules | null,
   bookRules: BookRules | null,
+  languageOverride?: "zh" | "en",
 ): ReadonlyArray<PostWriteViolation> {
 ): ReadonlyArray<PostWriteViolation> {
   const violations: PostWriteViolation[] = [];
   const violations: PostWriteViolation[] = [];
 
 
   // Skip Chinese-specific rules for English content
   // Skip Chinese-specific rules for English content
-  const isEnglish = genreProfile.language === "en";
+  const isEnglish = (languageOverride ?? genreProfile.language) === "en";
   if (isEnglish) {
   if (isEnglish) {
     // For English, only run book-specific prohibitions and paragraph length check
     // For English, only run book-specific prohibitions and paragraph length check
     return validatePostWriteEnglish(content, genreProfile, bookRules);
     return validatePostWriteEnglish(content, genreProfile, bookRules);
@@ -244,6 +245,74 @@ export function validatePostWrite(
   return violations;
   return violations;
 }
 }
 
 
+/**
+ * Cross-chapter repetition check.
+ * Detects phrases from the current chapter that also appeared in recent chapters.
+ */
+export function detectCrossChapterRepetition(
+  currentContent: string,
+  recentChaptersContent: string,
+  language: "zh" | "en" = "zh",
+): ReadonlyArray<PostWriteViolation> {
+  if (!recentChaptersContent || recentChaptersContent.length < 100) return [];
+
+  const violations: PostWriteViolation[] = [];
+  const isEnglish = language === "en";
+
+  if (isEnglish) {
+    // Extract 3-word phrases from current chapter
+    const words = currentContent.toLowerCase().replace(/[^\w\s']/g, "").split(/\s+/).filter(w => w.length > 2);
+    const phraseCounts = new Map<string, number>();
+    for (let i = 0; i < words.length - 2; i++) {
+      const phrase = `${words[i]} ${words[i + 1]} ${words[i + 2]}`;
+      phraseCounts.set(phrase, (phraseCounts.get(phrase) ?? 0) + 1);
+    }
+    // Check which repeated phrases (2+ in current) also appear in recent chapters
+    const recentLower = recentChaptersContent.toLowerCase();
+    const crossRepeats: string[] = [];
+    for (const [phrase, count] of phraseCounts) {
+      if (count >= 2 && recentLower.includes(phrase)) {
+        crossRepeats.push(`"${phrase}" (×${count})`);
+      }
+    }
+    if (crossRepeats.length >= 3) {
+      violations.push({
+        rule: "Cross-chapter repetition",
+        severity: "warning",
+        description: `${crossRepeats.length} repeated phrases also found in recent chapters: ${crossRepeats.slice(0, 5).join(", ")}`,
+        suggestion: "Vary action verbs and descriptive phrases to avoid cross-chapter repetition",
+      });
+    }
+  } else {
+    // Chinese: 6-char ngrams
+    const chars = currentContent.replace(/[\s\n\r]/g, "");
+    const phraseCounts = new Map<string, number>();
+    for (let i = 0; i < chars.length - 5; i++) {
+      const phrase = chars.slice(i, i + 6);
+      if (/^[\u4e00-\u9fff]{6}$/.test(phrase)) {
+        phraseCounts.set(phrase, (phraseCounts.get(phrase) ?? 0) + 1);
+      }
+    }
+    const recentClean = recentChaptersContent.replace(/[\s\n\r]/g, "");
+    const crossRepeats: string[] = [];
+    for (const [phrase, count] of phraseCounts) {
+      if (count >= 2 && recentClean.includes(phrase)) {
+        crossRepeats.push(`"${phrase}"(×${count})`);
+      }
+    }
+    if (crossRepeats.length >= 3) {
+      violations.push({
+        rule: "跨章重复",
+        severity: "warning",
+        description: `${crossRepeats.length}个重复短语在近期章节中也出现过:${crossRepeats.slice(0, 5).join("、")}`,
+        suggestion: "变换动作描写和场景用语,避免跨章节机械重复",
+      });
+    }
+  }
+
+  return violations;
+}
+
 /** English-specific post-write validation rules. */
 /** English-specific post-write validation rules. */
 function validatePostWriteEnglish(
 function validatePostWriteEnglish(
   content: string,
   content: string,

+ 187 - 24
packages/core/src/agents/writer.ts

@@ -7,13 +7,19 @@ import { buildSettlerSystemPrompt, buildSettlerUserPrompt } from "./settler-prom
 import { buildObserverSystemPrompt, buildObserverUserPrompt } from "./observer-prompts.js";
 import { buildObserverSystemPrompt, buildObserverUserPrompt } from "./observer-prompts.js";
 import { parseSettlementOutput } from "./settler-parser.js";
 import { parseSettlementOutput } from "./settler-parser.js";
 import { readGenreProfile, readBookRules } from "./rules-reader.js";
 import { readGenreProfile, readBookRules } from "./rules-reader.js";
-import { validatePostWrite, type PostWriteViolation } from "./post-write-validator.js";
+import { validatePostWrite, detectCrossChapterRepetition, type PostWriteViolation } from "./post-write-validator.js";
 import { analyzeAITells } from "./ai-tells.js";
 import { analyzeAITells } from "./ai-tells.js";
 import type { ChapterTrace, ContextPackage, RuleStack } from "../models/input-governance.js";
 import type { ChapterTrace, ContextPackage, RuleStack } from "../models/input-governance.js";
 import type { LengthSpec } from "../models/length-governance.js";
 import type { LengthSpec } from "../models/length-governance.js";
 import { buildLengthSpec } from "../utils/length-metrics.js";
 import { buildLengthSpec } from "../utils/length-metrics.js";
 import { filterHooks, filterSummaries, filterSubplots, filterEmotionalArcs, filterCharacterMatrix } from "../utils/context-filter.js";
 import { filterHooks, filterSummaries, filterSubplots, filterEmotionalArcs, filterCharacterMatrix } from "../utils/context-filter.js";
 import { buildGovernedMemoryEvidenceBlocks } from "../utils/governed-context.js";
 import { buildGovernedMemoryEvidenceBlocks } from "../utils/governed-context.js";
+import {
+  buildGovernedCharacterMatrixWorkingSet,
+  buildGovernedHookWorkingSet,
+  mergeCharacterMatrixMarkdown,
+  mergeTableMarkdownByKey,
+} from "../utils/governed-working-set.js";
 import { extractPOVFromOutline, filterMatrixByPOV, filterHooksByPOV } from "../utils/pov-filter.js";
 import { extractPOVFromOutline, filterMatrixByPOV, filterHooksByPOV } from "../utils/pov-filter.js";
 import { parseCreativeOutput } from "./writer-parser.js";
 import { parseCreativeOutput } from "./writer-parser.js";
 import { readFile, writeFile, mkdir, readdir } from "node:fs/promises";
 import { readFile, writeFile, mkdir, readdir } from "node:fs/promises";
@@ -63,6 +69,18 @@ export class WriterAgent extends BaseAgent {
     return "writer";
     return "writer";
   }
   }
 
 
+  private localize(language: "zh" | "en", messages: { zh: string; en: string }): string {
+    return language === "en" ? messages.en : messages.zh;
+  }
+
+  private logInfo(language: "zh" | "en", messages: { zh: string; en: string }): void {
+    this.ctx.logger?.info(this.localize(language, messages));
+  }
+
+  private logWarn(language: "zh" | "en", messages: { zh: string; en: string }): void {
+    this.ctx.logger?.warn(this.localize(language, messages));
+  }
+
   async writeChapter(input: WriteChapterInput): Promise<WriteChapterOutput> {
   async writeChapter(input: WriteChapterInput): Promise<WriteChapterOutput> {
     const { book, bookDir, chapterNumber } = input;
     const { book, bookDir, chapterNumber } = input;
 
 
@@ -178,7 +196,10 @@ export class WriterAgent extends BaseAgent {
 
 
     const creativeTemperature = input.temperatureOverride ?? 0.7;
     const creativeTemperature = input.temperatureOverride ?? 0.7;
 
 
-    this.ctx.logger?.info(`Phase 1: creative writing for chapter ${chapterNumber}`);
+    this.logInfo(resolvedLanguage, {
+      zh: `阶段 1:创作正文(第${chapterNumber}章)`,
+      en: `Phase 1: creative writing for chapter ${chapterNumber}`,
+    });
 
 
     // Scale maxTokens to chapter word count (Chinese ≈ 1.5 tokens/char)
     // Scale maxTokens to chapter word count (Chinese ≈ 1.5 tokens/char)
     const creativeMaxTokens = Math.max(8192, Math.ceil(targetWords * 2));
     const creativeMaxTokens = Math.max(8192, Math.ceil(targetWords * 2));
@@ -195,8 +216,33 @@ export class WriterAgent extends BaseAgent {
     const creative = parseCreativeOutput(chapterNumber, creativeResponse.content, resolvedLengthSpec.countingMode);
     const creative = parseCreativeOutput(chapterNumber, creativeResponse.content, resolvedLengthSpec.countingMode);
 
 
     // ── Phase 2: State settlement (temperature 0.3) ──
     // ── Phase 2: State settlement (temperature 0.3) ──
-    const lengthUnit = resolvedLengthSpec.countingMode === "en_words" ? "words" : "chars";
-    this.ctx.logger?.info(`Phase 2: state settlement for chapter ${chapterNumber} (${creative.wordCount} ${lengthUnit})`);
+    this.logInfo(resolvedLanguage, {
+      zh: `阶段 2:状态结算(第${chapterNumber}章,${creative.wordCount}字)`,
+      en: `Phase 2: state settlement for chapter ${chapterNumber} (${creative.wordCount} words)`,
+    });
+    const isGovernedSettlement = Boolean(input.chapterIntent && input.contextPackage && input.ruleStack);
+    const filteredHooksForSettlement = isGovernedSettlement && input.contextPackage
+      ? buildGovernedHookWorkingSet({
+          hooksMarkdown: hooks,
+          contextPackage: input.contextPackage,
+          chapterNumber,
+          language: resolvedLanguage,
+        })
+      : hooks;
+    const filteredSubplotsForSettlement = isGovernedSettlement
+      ? filterSubplots(subplotBoard)
+      : subplotBoard;
+    const filteredArcsForSettlement = isGovernedSettlement
+      ? filterEmotionalArcs(emotionalArcs, chapterNumber)
+      : emotionalArcs;
+    const filteredMatrixForSettlement = isGovernedSettlement
+      ? buildGovernedCharacterMatrixWorkingSet({
+          matrixMarkdown: characterMatrix,
+          chapterIntent: input.chapterIntent ?? volumeOutline,
+          contextPackage: input.contextPackage!,
+          protagonistName: bookRules?.protagonist?.name,
+        })
+      : characterMatrix;
 
 
     const settleResult = await this.settle({
     const settleResult = await this.settle({
       book,
       book,
@@ -207,40 +253,56 @@ export class WriterAgent extends BaseAgent {
       content: creative.content,
       content: creative.content,
       currentState,
       currentState,
       ledger: genreProfile.numericalSystem ? ledger : "",
       ledger: genreProfile.numericalSystem ? ledger : "",
-      hooks,
+      hooks: filteredHooksForSettlement,
       chapterSummaries: input.contextPackage ? filterSummaries(chapterSummaries, chapterNumber) : chapterSummaries,
       chapterSummaries: input.contextPackage ? filterSummaries(chapterSummaries, chapterNumber) : chapterSummaries,
-      subplotBoard,
-      emotionalArcs,
-      characterMatrix,
+      subplotBoard: filteredSubplotsForSettlement,
+      emotionalArcs: filteredArcsForSettlement,
+      characterMatrix: filteredMatrixForSettlement,
       volumeOutline,
       volumeOutline,
       selectedEvidenceBlock: governedMemoryBlocks
       selectedEvidenceBlock: governedMemoryBlocks
-        ? [governedMemoryBlocks.hooksBlock, governedMemoryBlocks.summariesBlock]
+        ? [
+            governedMemoryBlocks.hooksBlock,
+            governedMemoryBlocks.summariesBlock,
+            governedMemoryBlocks.volumeSummariesBlock,
+          ]
           .filter(Boolean)
           .filter(Boolean)
           .join("\n")
           .join("\n")
         : undefined,
         : undefined,
+      chapterIntent: input.chapterIntent,
+      contextPackage: input.contextPackage,
+      ruleStack: input.ruleStack,
+      originalHooks: hooks,
+      originalSubplots: subplotBoard,
+      originalEmotionalArcs: emotionalArcs,
+      originalCharacterMatrix: characterMatrix,
     });
     });
     const settlement = settleResult.settlement;
     const settlement = settleResult.settlement;
     const settleUsage = settleResult.usage;
     const settleUsage = settleResult.usage;
 
 
     // ── Post-write validation (regex + rule-based, zero LLM cost) ──
     // ── Post-write validation (regex + rule-based, zero LLM cost) ──
-    const ruleViolations = validatePostWrite(creative.content, genreProfile, bookRules);
+    const ruleViolations = [
+      ...validatePostWrite(creative.content, genreProfile, bookRules, resolvedLanguage),
+      ...detectCrossChapterRepetition(creative.content, fingerprintChapters, resolvedLanguage),
+    ];
     const aiTellIssues = analyzeAITells(creative.content).issues;
     const aiTellIssues = analyzeAITells(creative.content).issues;
 
 
     const postWriteErrors = ruleViolations.filter(v => v.severity === "error");
     const postWriteErrors = ruleViolations.filter(v => v.severity === "error");
     const postWriteWarnings = ruleViolations.filter(v => v.severity === "warning");
     const postWriteWarnings = ruleViolations.filter(v => v.severity === "warning");
 
 
     if (ruleViolations.length > 0) {
     if (ruleViolations.length > 0) {
-      this.ctx.logger?.warn(
-        `Post-write: ${postWriteErrors.length} errors, ${postWriteWarnings.length} warnings in chapter ${chapterNumber}`,
-      );
+      this.logWarn(resolvedLanguage, {
+        zh: `后写校验:第${chapterNumber}章 ${postWriteErrors.length} 个错误,${postWriteWarnings.length} 个警告`,
+        en: `Post-write: ${postWriteErrors.length} errors, ${postWriteWarnings.length} warnings in chapter ${chapterNumber}`,
+      });
       for (const v of ruleViolations) {
       for (const v of ruleViolations) {
         this.ctx.logger?.warn(`[${v.severity}] ${v.rule}: ${v.description}`);
         this.ctx.logger?.warn(`[${v.severity}] ${v.rule}: ${v.description}`);
       }
       }
     }
     }
     if (aiTellIssues.length > 0) {
     if (aiTellIssues.length > 0) {
-      this.ctx.logger?.warn(
-        `AI-tell check: ${aiTellIssues.length} issues in chapter ${chapterNumber}`,
-      );
+      this.logWarn(resolvedLanguage, {
+        zh: `AI 味检查:第${chapterNumber}章发现 ${aiTellIssues.length} 个问题`,
+        en: `AI-tell check: ${aiTellIssues.length} issues in chapter ${chapterNumber}`,
+      });
       for (const issue of aiTellIssues) {
       for (const issue of aiTellIssues) {
         this.ctx.logger?.warn(`[${issue.severity}] ${issue.category}: ${issue.description}`);
         this.ctx.logger?.warn(`[${issue.severity}] ${issue.category}: ${issue.description}`);
       }
       }
@@ -289,13 +351,23 @@ export class WriterAgent extends BaseAgent {
     readonly characterMatrix: string;
     readonly characterMatrix: string;
     readonly volumeOutline: string;
     readonly volumeOutline: string;
     readonly selectedEvidenceBlock?: string;
     readonly selectedEvidenceBlock?: string;
+    readonly chapterIntent?: string;
+    readonly contextPackage?: ContextPackage;
+    readonly ruleStack?: RuleStack;
+    readonly originalHooks: string;
+    readonly originalSubplots: string;
+    readonly originalEmotionalArcs: string;
+    readonly originalCharacterMatrix: string;
   }): Promise<{ settlement: ReturnType<typeof parseSettlementOutput>; usage: TokenUsage }> {
   }): Promise<{ settlement: ReturnType<typeof parseSettlementOutput>; usage: TokenUsage }> {
     // Phase 2a: Observer — extract all facts from the chapter
     // Phase 2a: Observer — extract all facts from the chapter
     const resolvedLang = params.book.language ?? params.genreProfile.language;
     const resolvedLang = params.book.language ?? params.genreProfile.language;
     const observerSystem = buildObserverSystemPrompt(params.book, params.genreProfile, resolvedLang);
     const observerSystem = buildObserverSystemPrompt(params.book, params.genreProfile, resolvedLang);
     const observerUser = buildObserverUserPrompt(params.chapterNumber, params.title, params.content, resolvedLang);
     const observerUser = buildObserverUserPrompt(params.chapterNumber, params.title, params.content, resolvedLang);
 
 
-    this.ctx.logger?.info(`Phase 2a: observing facts for chapter ${params.chapterNumber}`);
+    this.logInfo(resolvedLang, {
+      zh: `阶段 2a:提取第${params.chapterNumber}章事实`,
+      en: `Phase 2a: observing facts for chapter ${params.chapterNumber}`,
+    });
     const observerResponse = await this.chat(
     const observerResponse = await this.chat(
       [
       [
         { role: "system", content: observerSystem },
         { role: "system", content: observerSystem },
@@ -306,10 +378,21 @@ export class WriterAgent extends BaseAgent {
     const observations = observerResponse.content;
     const observations = observerResponse.content;
 
 
     // Phase 2b: Reflector — merge observations into truth files
     // Phase 2b: Reflector — merge observations into truth files
-    this.ctx.logger?.info(`Phase 2b: reflecting observations into truth files`);
+    this.logInfo(resolvedLang, {
+      zh: "阶段 2b:把观察结果回写到真相文件",
+      en: "Phase 2b: reflecting observations into truth files",
+    });
     const settlerSystem = buildSettlerSystemPrompt(
     const settlerSystem = buildSettlerSystemPrompt(
       params.book, params.genreProfile, params.bookRules, resolvedLang,
       params.book, params.genreProfile, params.bookRules, resolvedLang,
     );
     );
+    const governedControlBlock = params.chapterIntent && params.contextPackage && params.ruleStack
+      ? this.buildSettlerGovernedControlBlock(
+          params.chapterIntent,
+          params.contextPackage,
+          params.ruleStack,
+          resolvedLang,
+        )
+      : undefined;
 
 
     const settlerUser = buildSettlerUserPrompt({
     const settlerUser = buildSettlerUserPrompt({
       chapterNumber: params.chapterNumber,
       chapterNumber: params.chapterNumber,
@@ -325,6 +408,7 @@ export class WriterAgent extends BaseAgent {
       volumeOutline: params.volumeOutline,
       volumeOutline: params.volumeOutline,
       observations,
       observations,
       selectedEvidenceBlock: params.selectedEvidenceBlock,
       selectedEvidenceBlock: params.selectedEvidenceBlock,
+      governedControlBlock,
     });
     });
 
 
     // Settler outputs all truth files — scale with content size
     // Settler outputs all truth files — scale with content size
@@ -338,8 +422,25 @@ export class WriterAgent extends BaseAgent {
       { maxTokens: settlerMaxTokens, temperature: 0.3 },
       { maxTokens: settlerMaxTokens, temperature: 0.3 },
     );
     );
 
 
+    const settlement = parseSettlementOutput(response.content, params.genreProfile);
+    const mergedSettlement = governedControlBlock
+      ? {
+          ...settlement,
+          updatedHooks: mergeTableMarkdownByKey(params.originalHooks, settlement.updatedHooks, [0]),
+          updatedSubplots: settlement.updatedSubplots
+            ? mergeTableMarkdownByKey(params.originalSubplots, settlement.updatedSubplots, [0])
+            : settlement.updatedSubplots,
+          updatedEmotionalArcs: settlement.updatedEmotionalArcs
+            ? mergeTableMarkdownByKey(params.originalEmotionalArcs, settlement.updatedEmotionalArcs, [0, 1])
+            : settlement.updatedEmotionalArcs,
+          updatedCharacterMatrix: settlement.updatedCharacterMatrix
+            ? mergeCharacterMatrixMarkdown(params.originalCharacterMatrix, settlement.updatedCharacterMatrix)
+            : settlement.updatedCharacterMatrix,
+        }
+      : settlement;
+
     return {
     return {
-      settlement: parseSettlementOutput(response.content, params.genreProfile),
+      settlement: mergedSettlement,
       usage: response.usage,
       usage: response.usage,
     };
     };
   }
   }
@@ -577,6 +678,52 @@ ${lengthRequirementBlock}
 - 只需输出 PRE_WRITE_CHECK、CHAPTER_TITLE、CHAPTER_CONTENT 三个区块`;
 - 只需输出 PRE_WRITE_CHECK、CHAPTER_TITLE、CHAPTER_CONTENT 三个区块`;
   }
   }
 
 
+  private buildSettlerGovernedControlBlock(
+    chapterIntent: string,
+    contextPackage: ContextPackage,
+    ruleStack: RuleStack,
+    language: "zh" | "en",
+  ): string {
+    const selectedContext = contextPackage.selectedContext
+      .map((entry) => `- ${entry.source}: ${entry.reason}${entry.excerpt ? ` | ${entry.excerpt}` : ""}`)
+      .join("\n");
+    const overrides = ruleStack.activeOverrides.length > 0
+      ? ruleStack.activeOverrides
+        .map((override) => `- ${override.from} -> ${override.to}: ${override.reason} (${override.target})`)
+        .join("\n")
+      : "- none";
+
+    if (language === "en") {
+      return `\n## Chapter Control Inputs
+${chapterIntent}
+
+### Selected Context
+${selectedContext || "- none"}
+
+### Rule Stack
+- Hard guardrails: ${ruleStack.sections.hard.join(", ") || "(none)"}
+- Soft constraints: ${ruleStack.sections.soft.join(", ") || "(none)"}
+- Diagnostic rules: ${ruleStack.sections.diagnostic.join(", ") || "(none)"}
+
+### Active Overrides
+${overrides}\n`;
+    }
+
+    return `\n## 本章控制输入
+${chapterIntent}
+
+### 已选上下文
+${selectedContext || "- none"}
+
+### 规则栈
+- 硬护栏:${ruleStack.sections.hard.join("、") || "(无)"}
+- 软约束:${ruleStack.sections.soft.join("、") || "(无)"}
+- 诊断规则:${ruleStack.sections.diagnostic.join("、") || "(无)"}
+
+### 当前覆盖
+${overrides}\n`;
+  }
+
   private buildLengthRequirementBlock(lengthSpec: LengthSpec, language: "zh" | "en"): string {
   private buildLengthRequirementBlock(lengthSpec: LengthSpec, language: "zh" | "en"): string {
     if (language === "en") {
     if (language === "en") {
       return `Requirements:
       return `Requirements:
@@ -626,13 +773,17 @@ ${lengthRequirementBlock}
   }
   }
 
 
   /** Save new truth files (summaries, subplots, emotional arcs, character matrix). */
   /** Save new truth files (summaries, subplots, emotional arcs, character matrix). */
-  async saveNewTruthFiles(bookDir: string, output: WriteChapterOutput): Promise<void> {
+  async saveNewTruthFiles(
+    bookDir: string,
+    output: WriteChapterOutput,
+    language: "zh" | "en" = "zh",
+  ): Promise<void> {
     const storyDir = join(bookDir, "story");
     const storyDir = join(bookDir, "story");
     const writes: Array<Promise<void>> = [];
     const writes: Array<Promise<void>> = [];
 
 
     // Append chapter summary to chapter_summaries.md
     // Append chapter summary to chapter_summaries.md
     if (output.chapterSummary) {
     if (output.chapterSummary) {
-      writes.push(this.appendChapterSummary(storyDir, output.chapterSummary));
+      writes.push(this.appendChapterSummary(storyDir, output.chapterSummary, language));
     }
     }
 
 
     // Overwrite subplot board
     // Overwrite subplot board
@@ -653,20 +804,32 @@ ${lengthRequirementBlock}
     await Promise.all(writes);
     await Promise.all(writes);
   }
   }
 
 
-  private async appendChapterSummary(storyDir: string, summary: string): Promise<void> {
+  private async appendChapterSummary(
+    storyDir: string,
+    summary: string,
+    language: "zh" | "en",
+  ): Promise<void> {
     const summaryPath = join(storyDir, "chapter_summaries.md");
     const summaryPath = join(storyDir, "chapter_summaries.md");
     let existing = "";
     let existing = "";
     try {
     try {
       existing = await readFile(summaryPath, "utf-8");
       existing = await readFile(summaryPath, "utf-8");
     } catch {
     } catch {
       // File doesn't exist yet — start with header
       // File doesn't exist yet — start with header
-      existing = "# 章节摘要\n\n| 章节 | 标题 | 出场人物 | 关键事件 | 状态变化 | 伏笔动态 | 情绪基调 | 章节类型 |\n|------|------|----------|----------|----------|----------|----------|----------|\n";
+      existing = language === "en"
+        ? "# Chapter Summaries\n\n| Chapter | Title | Characters | Key Events | State Changes | Hook Activity | Mood | Chapter Type |\n| --- | --- | --- | --- | --- | --- | --- | --- |\n"
+        : "# 章节摘要\n\n| 章节 | 标题 | 出场人物 | 关键事件 | 状态变化 | 伏笔动态 | 情绪基调 | 章节类型 |\n|------|------|----------|----------|----------|----------|----------|----------|\n";
     }
     }
 
 
     // Extract only the data row(s) from the summary (skip header lines)
     // Extract only the data row(s) from the summary (skip header lines)
     const dataRows = summary
     const dataRows = summary
       .split("\n")
       .split("\n")
-      .filter((line) => line.startsWith("|") && !line.startsWith("| 章节") && !line.startsWith("|--"))
+      .filter((line) =>
+        line.startsWith("|")
+        && !line.startsWith("| 章节")
+        && !line.startsWith("| Chapter")
+        && !line.startsWith("|--")
+        && !line.startsWith("| ---"),
+      )
       .join("\n");
       .join("\n");
 
 
     if (dataRows) {
     if (dataRows) {