Просмотр исходного кода

fix: English language pipeline across all agents

- Architect: language override prefix forces English output for story_bible, outline, book_rules
- Settler: language override prefix forces English truth files
- Auditor: bilingual system prompt (English for en genres)
- Reviser: language override prefix forces English revised content
- Post-write validator: skip Chinese-only rules for English, add English AI-tell checks
- Writer saveChapter: "Chapter X:" heading for English, "第X章" for Chinese
- Runner: pass language to saveChapter and chapter file headings
- Export: epub lang tag respects book language setting
Ma 7 месяцев назад
Родитель
Сommit
0f8033c701

+ 2 - 2
packages/cli/src/commands/export.ts

@@ -84,7 +84,7 @@ export const exportCommand = new Command("export")
   });
 
 async function exportEpub(
-  book: { readonly title: string },
+  book: { readonly title: string; readonly language?: string },
   chapters: ReadonlyArray<{ readonly number: number; readonly wordCount: number }>,
   chaptersDir: string,
   bookId: string,
@@ -112,7 +112,7 @@ async function exportEpub(
   }
 
   const epubInstance = new EPub(
-    { title: book.title, lang: "zh-CN" },
+    { title: book.title, lang: book.language === "en" ? "en" : "zh-CN" },
     epubChapters,
   );
   const epubBuffer: Buffer = await epubInstance.genEpub();

+ 10 - 5
packages/core/src/agents/architect.ts

@@ -151,12 +151,17 @@ ${eraBlock}
 4. 伏笔前后呼应,不留悬空线
 5. 配角有独立动机,不是工具人`;
 
+    const resolvedLanguage = book.language ?? gp.language;
+    const langPrefix = resolvedLanguage === "en"
+      ? `【LANGUAGE OVERRIDE】ALL output (story_bible, volume_outline, book_rules, current_state, pending_hooks) MUST be written in English. Character names, place names, and all prose must be in English. The === SECTION: === tags remain unchanged.\n\n`
+      : "";
+    const userMessage = resolvedLanguage === "en"
+      ? `Generate the complete foundation for a ${gp.name} novel titled "${book.title}". Write everything in English.`
+      : `请为标题为"${book.title}"的${gp.name}小说生成完整基础设定。`;
+
     const response = await this.chat([
-      { role: "system", content: systemPrompt },
-      {
-        role: "user",
-        content: `请为标题为"${book.title}"的${gp.name}小说生成完整基础设定。`,
-      },
+      { role: "system", content: langPrefix + systemPrompt },
+      { role: "user", content: userMessage },
     ], { maxTokens: 16384, temperature: 0.8 });
 
     return this.parseSections(response.content);

+ 25 - 1
packages/core/src/agents/continuity.ts

@@ -253,7 +253,31 @@ export class ContinuityAuditor extends BaseAgent {
       ? "\n\n你有联网搜索能力(search_web / fetch_url)。对于涉及真实年代、人物、事件、地理、政策的内容,你必须用search_web核实,不可凭记忆判断。至少对比2个来源交叉验证。"
       : "";
 
-    const systemPrompt = `你是一位严格的${gp.name}网络小说审稿编辑。你的任务是对章节进行连续性、一致性和质量审查。${protagonistBlock}${searchNote}
+    const resolvedLanguage = gp.language;
+    const isEnglish = resolvedLanguage === "en";
+
+    const systemPrompt = isEnglish
+      ? `You are a strict ${gp.name} web fiction editor. Audit the chapter for continuity, consistency, and quality. ALL OUTPUT MUST BE IN ENGLISH.${protagonistBlock}${searchNote}
+
+Audit dimensions:
+${dimList}
+
+Output format MUST be JSON:
+{
+  "passed": true/false,
+  "issues": [
+    {
+      "severity": "critical|warning|info",
+      "category": "dimension name",
+      "description": "specific issue description",
+      "suggestion": "fix suggestion"
+    }
+  ],
+  "summary": "one-sentence audit conclusion"
+}
+
+passed is false ONLY when critical-severity issues exist.`
+      : `你是一位严格的${gp.name}网络小说审稿编辑。你的任务是对章节进行连续性、一致性和质量审查。${protagonistBlock}${searchNote}
 
 审查维度:
 ${dimList}

+ 76 - 0
packages/core/src/agents/post-write-validator.ts

@@ -55,6 +55,13 @@ export function validatePostWrite(
 ): ReadonlyArray<PostWriteViolation> {
   const violations: PostWriteViolation[] = [];
 
+  // Skip Chinese-specific rules for English content
+  const isEnglish = genreProfile.language === "en";
+  if (isEnglish) {
+    // For English, only run book-specific prohibitions and paragraph length check
+    return validatePostWriteEnglish(content, genreProfile, bookRules);
+  }
+
   // 1. 硬性禁令: "不是…而是…" 句式
   if (/不是[^,。!?\n]{0,30}[,,]?\s*而是/.test(content)) {
     violations.push({
@@ -236,3 +243,72 @@ export function validatePostWrite(
 
   return violations;
 }
+
+/** English-specific post-write validation rules. */
+function validatePostWriteEnglish(
+  content: string,
+  genreProfile: GenreProfile,
+  bookRules: BookRules | null,
+): ReadonlyArray<PostWriteViolation> {
+  const violations: PostWriteViolation[] = [];
+
+  // 1. AI-tell word density (from en-prompt-sections IRON LAW 3)
+  const aiTellWords = ["delve", "tapestry", "testament", "intricate", "pivotal", "vibrant", "embark", "comprehensive", "nuanced"];
+  for (const word of aiTellWords) {
+    const regex = new RegExp(`\\b${word}\\b`, "gi");
+    const matches = content.match(regex);
+    if (matches && matches.length > Math.ceil(content.length / 3000)) {
+      violations.push({
+        rule: "AI-tell word density",
+        severity: "warning",
+        description: `"${word}" appears ${matches.length} times (limit: 1 per 3000 chars)`,
+        suggestion: `Replace with a more specific word`,
+      });
+    }
+  }
+
+  // 2. Paragraph overflow (same rule applies to English)
+  const paragraphs = content.split(/\n\s*\n/).filter((p) => p.trim().length > 0);
+  const longParagraphs = paragraphs.filter((p) => p.length > 500);
+  if (longParagraphs.length >= 2) {
+    violations.push({
+      rule: "Paragraph length",
+      severity: "warning",
+      description: `${longParagraphs.length} paragraphs exceed 500 characters`,
+      suggestion: "Break into shorter paragraphs for readability",
+    });
+  }
+
+  // 3. Book-specific prohibitions
+  if (bookRules?.prohibitions) {
+    for (const prohibition of bookRules.prohibitions) {
+      if (prohibition.length >= 2 && prohibition.length <= 50 && content.toLowerCase().includes(prohibition.toLowerCase())) {
+        violations.push({
+          rule: "Book prohibition",
+          severity: "error",
+          description: `Found banned content: "${prohibition}"`,
+          suggestion: "Remove or rewrite this content",
+        });
+      }
+    }
+  }
+
+  // 4. Genre fatigue words
+  const fatigueWords = bookRules?.fatigueWordsOverride && bookRules.fatigueWordsOverride.length > 0
+    ? bookRules.fatigueWordsOverride
+    : genreProfile.fatigueWords;
+  for (const word of fatigueWords) {
+    const regex = new RegExp(`\\b${word}\\b`, "gi");
+    const matches = content.match(regex);
+    if (matches && matches.length > 1) {
+      violations.push({
+        rule: "Fatigue word",
+        severity: "warning",
+        description: `"${word}" appears ${matches.length} times (max 1 per chapter)`,
+        suggestion: "Vary the vocabulary",
+      });
+    }
+  }
+
+  return violations;
+}

+ 6 - 1
packages/core/src/agents/reviser.ts

@@ -90,7 +90,12 @@ export class ReviserAgent extends BaseAgent {
       ? `\n\n主角人设锁定:${bookRules.protagonist.name},${bookRules.protagonist.personalityLock.join("、")}。修改不得违反人设。`
       : "";
 
-    const systemPrompt = `你是一位专业的${gp.name}网络小说修稿编辑。你的任务是根据审稿意见对章节进行修正。${protagonistBlock}
+    const isEnglish = gp.language === "en";
+    const langPrefix = isEnglish
+      ? `【LANGUAGE OVERRIDE】ALL output (FIXED_ISSUES, REVISED_CONTENT, UPDATED_STATE, UPDATED_HOOKS) MUST be in English. The revised chapter content must be written entirely in English.\n\n`
+      : "";
+
+    const systemPrompt = `${langPrefix}你是一位专业的${gp.name}网络小说修稿编辑。你的任务是根据审稿意见对章节进行修正。${protagonistBlock}
 
 修稿模式:${modeDesc}
 

+ 8 - 1
packages/core/src/agents/settler-prompts.ts

@@ -6,7 +6,10 @@ export function buildSettlerSystemPrompt(
   book: BookConfig,
   genreProfile: GenreProfile,
   bookRules: BookRules | null,
+  language?: "zh" | "en",
 ): string {
+  const resolvedLang = language ?? genreProfile.language;
+  const isEnglish = resolvedLang === "en";
   const numericalBlock = genreProfile.numericalSystem
     ? `\n- 本题材有数值/资源体系,你必须在 UPDATED_LEDGER 中追踪正文中出现的所有资源变动
 - 数值验算铁律:期初 + 增量 = 期末,三项必须可验算`
@@ -24,7 +27,11 @@ export function buildSettlerSystemPrompt(
     ? `\n## 全员追踪\nPOST_SETTLEMENT 必须额外包含:本章出场角色清单、角色间关系变动、未出场但被提及的角色。`
     : "";
 
-  return `你是状态追踪分析师。给定新章节正文和当前 truth 文件,你的任务是产出更新后的 truth 文件。
+  const langPrefix = isEnglish
+    ? `【LANGUAGE OVERRIDE】ALL output (state card, hooks, summaries, subplots, emotional arcs, character matrix) MUST be in English. The === TAG === markers remain unchanged.\n\n`
+    : "";
+
+  return `${langPrefix}你是状态追踪分析师。给定新章节正文和当前 truth 文件,你的任务是产出更新后的 truth 文件。
 
 ## 工作模式
 

+ 6 - 1
packages/core/src/agents/writer.ts

@@ -237,6 +237,7 @@ export class WriterAgent extends BaseAgent {
   }): Promise<{ settlement: ReturnType<typeof parseSettlementOutput>; usage: TokenUsage }> {
     const settlerSystem = buildSettlerSystemPrompt(
       params.book, params.genreProfile, params.bookRules,
+      params.book.language ?? params.genreProfile.language,
     );
 
     const settlerUser = buildSettlerUserPrompt({
@@ -274,6 +275,7 @@ export class WriterAgent extends BaseAgent {
     bookDir: string,
     output: WriteChapterOutput,
     numericalSystem: boolean = true,
+    language: "zh" | "en" = "zh",
   ): Promise<void> {
     const chaptersDir = join(bookDir, "chapters");
     const storyDir = join(bookDir, "story");
@@ -282,8 +284,11 @@ export class WriterAgent extends BaseAgent {
     const paddedNum = String(output.chapterNumber).padStart(4, "0");
     const filename = `${paddedNum}_${this.sanitizeFilename(output.title)}.md`;
 
+    const heading = language === "en"
+      ? `# Chapter ${output.chapterNumber}: ${output.title}`
+      : `# 第${output.chapterNumber}章 ${output.title}`;
     const chapterContent = [
-      `# 第${output.chapterNumber}章 ${output.title}`,
+      heading,
       "",
       output.content,
     ].join("\n");

+ 11 - 3
packages/core/src/pipeline/runner.ts

@@ -273,10 +273,14 @@ export class PipelineRunner {
       const filename = `${paddedNum}_${sanitized}.md`;
       const filePath = join(chaptersDir, filename);
 
-      await writeFile(filePath, `# 第${chapterNumber}章 ${output.title}\n\n${output.content}`, "utf-8");
+      const resolvedLang = book.language ?? gp.language;
+      const heading = resolvedLang === "en"
+        ? `# Chapter ${chapterNumber}: ${output.title}`
+        : `# 第${chapterNumber}章 ${output.title}`;
+      await writeFile(filePath, `${heading}\n\n${output.content}`, "utf-8");
 
       // Save truth files
-      await writer.saveChapter(bookDir, output, gp.numericalSystem);
+      await writer.saveChapter(bookDir, output, gp.numericalSystem, resolvedLang);
       await writer.saveNewTruthFiles(bookDir, output);
 
       // Update index
@@ -655,9 +659,13 @@ export class PipelineRunner {
     const title = output.title;
     const filename = `${paddedNum}_${title.replace(/[/\\?%*:|"<>]/g, "").replace(/\s+/g, "_").slice(0, 50)}.md`;
 
+    const pipelineLang = book.language ?? gp.language;
+    const pipelineHeading = pipelineLang === "en"
+      ? `# Chapter ${chapterNumber}: ${title}`
+      : `# 第${chapterNumber}章 ${title}`;
     await writeFile(
       join(chaptersDir, filename),
-      `# 第${chapterNumber}章 ${title}\n\n${finalContent}`,
+      `${pipelineHeading}\n\n${finalContent}`,
       "utf-8",
     );