Kaynağa Gözat

feat(planner): emit mood directive when consecutive chapters are high-tension

Implements buildMoodDirective — detects when 3+ recent chapters share
high-tension mood keywords (紧张/冷硬/压抑/tense/grim/etc.) and emits
a structured directive forcing the writer to downshift to a quieter scene.

Completes the moodDirective slot that was declared but left as undefined
in the previous structured-directives commit.
Ma 7 ay önce
ebeveyn
işleme
1fa53d3131

+ 94 - 0
packages/core/src/__tests__/planner.test.ts

@@ -324,6 +324,100 @@ describe("PlannerAgent", () => {
     expect(result.intent.moodDirective).toBeUndefined();
     expect(result.intent.moodDirective).toBeUndefined();
   });
   });
 
 
+  it("emits a mood directive when recent chapters are all high-tension", async () => {
+    book = {
+      ...book,
+      genre: "other",
+      language: "zh",
+    };
+
+    await Promise.all([
+      writeFile(
+        join(storyDir, "volume_outline.md"),
+        "# Volume Outline\n\n## Chapter 5\n进入新的地点。\n",
+        "utf-8",
+      ),
+      writeFile(
+        join(storyDir, "chapter_summaries.md"),
+        [
+          "# Chapter Summaries",
+          "",
+          "| chapter | title | characters | events | stateChanges | hookActivity | mood | chapterType |",
+          "| --- | --- | --- | --- | --- | --- | --- | --- |",
+          "| 1 | 暗巷追踪 | 周谨川 | 追踪目标 | None | none | 紧张、压抑 | 悬念验证章 |",
+          "| 2 | 旧楼对峙 | 周谨川 | 对峙 | None | none | 冷硬、逼仄 | 冲突章 |",
+          "| 3 | 夜色围堵 | 周谨川 | 围堵 | None | none | 肃杀、凝重 | 追击章 |",
+          "| 4 | 地下通道 | 周谨川 | 逃脱 | None | none | 压迫、窒息 | 逃亡章 |",
+          "",
+        ].join("\n"),
+        "utf-8",
+      ),
+    ]);
+
+    const planner = new PlannerAgent({
+      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
+      model: "test-model",
+      projectRoot: root,
+      bookId: book.id,
+    });
+
+    const result = await planner.planChapter({
+      book,
+      bookDir,
+      chapterNumber: 5,
+    });
+
+    expect(result.intent.moodDirective).toBeDefined();
+    expect(result.intent.moodDirective).toContain("降调");
+    expect(result.intent.moodDirective).toContain("日常");
+  });
+
+  it("does not emit a mood directive when recent moods are varied", async () => {
+    book = {
+      ...book,
+      genre: "other",
+      language: "en",
+    };
+
+    await Promise.all([
+      writeFile(
+        join(storyDir, "volume_outline.md"),
+        "# Volume Outline\n\n## Chapter 5\nMove to the harbor.\n",
+        "utf-8",
+      ),
+      writeFile(
+        join(storyDir, "chapter_summaries.md"),
+        [
+          "# Chapter Summaries",
+          "",
+          "| chapter | title | characters | events | stateChanges | hookActivity | mood | chapterType |",
+          "| --- | --- | --- | --- | --- | --- | --- | --- |",
+          "| 1 | Morning Calm | Taryn | A quiet walk | None | none | warm, gentle | slice-of-life |",
+          "| 2 | Sudden Rain | Taryn | Storm arrives | None | none | tense, ominous | tension |",
+          "| 3 | Harbor Light | Taryn | Finds shelter | None | none | hopeful, light | transition |",
+          "| 4 | The Letter | Taryn | Reads bad news | None | none | melancholy, reflective | introspection |",
+          "",
+        ].join("\n"),
+        "utf-8",
+      ),
+    ]);
+
+    const planner = new PlannerAgent({
+      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
+      model: "test-model",
+      projectRoot: root,
+      bookId: book.id,
+    });
+
+    const result = await planner.planChapter({
+      book,
+      bookDir,
+      chapterNumber: 5,
+    });
+
+    expect(result.intent.moodDirective).toBeUndefined();
+  });
+
   it("ignores the default current_focus placeholder and falls back to author intent when no chapter outline is available", async () => {
   it("ignores the default current_focus placeholder and falls back to author intent when no chapter outline is available", async () => {
     await Promise.all([
     await Promise.all([
       writeFile(
       writeFile(

+ 36 - 1
packages/core/src/agents/planner.ts

@@ -146,7 +146,7 @@ export class PlannerAgent extends BaseAgent {
         input.matchedOutlineAnchor,
         input.matchedOutlineAnchor,
       ),
       ),
       sceneDirective: this.buildSceneDirective(input.language, recentSummaries),
       sceneDirective: this.buildSceneDirective(input.language, recentSummaries),
-      moodDirective: undefined,
+      moodDirective: this.buildMoodDirective(input.language, recentSummaries),
       titleDirective: this.buildTitleDirective(input.language, recentSummaries),
       titleDirective: this.buildTitleDirective(input.language, recentSummaries),
     };
     };
   }
   }
@@ -346,6 +346,41 @@ export class PlannerAgent extends BaseAgent {
       : `Recent chapters are stuck in repeated ${repeatedType} beats. Change the scene container, location, or action pattern this chapter.`;
       : `Recent chapters are stuck in repeated ${repeatedType} beats. Change the scene container, location, or action pattern this chapter.`;
   }
   }
 
 
+  private buildMoodDirective(
+    language: string | undefined,
+    recentSummaries: ReadonlyArray<{ mood: string }>,
+  ): string | undefined {
+    if (recentSummaries.length < 3) {
+      return undefined;
+    }
+
+    const moods = recentSummaries.map((summary) => summary.mood.trim()).filter(Boolean);
+    if (moods.length < 3) {
+      return undefined;
+    }
+
+    const allHighTension = moods.every((mood) => this.isHighTensionMood(mood));
+    if (!allHighTension) {
+      return undefined;
+    }
+
+    return this.isChineseLanguage(language)
+      ? `最近${moods.length}章情绪持续高压(${moods.slice(0, 3).join("、")}),本章必须降调——安排日常/喘息/温情/幽默场景,让读者呼吸。`
+      : `The last ${moods.length} chapters have been relentlessly tense (${moods.slice(0, 3).join(", ")}). This chapter must downshift — write a quieter scene with warmth, humor, or breathing room.`;
+  }
+
+  private isHighTensionMood(mood: string): boolean {
+    const tensionKeywords = [
+      "紧张", "冷硬", "压抑", "逼仄", "肃杀", "沉重", "凝重",
+      "冷峻", "压迫", "阴沉", "焦灼", "窒息", "凛冽", "锋利",
+      "克制", "危机", "对峙", "绷紧", "僵持", "杀意",
+      "tense", "cold", "oppressive", "grim", "ominous", "dark",
+      "bleak", "hostile", "threatening", "heavy", "suffocating",
+    ];
+    const lowerMood = mood.toLowerCase();
+    return tensionKeywords.some((keyword) => lowerMood.includes(keyword));
+  }
+
   private buildTitleDirective(
   private buildTitleDirective(
     language: string | undefined,
     language: string | undefined,
     recentSummaries: ReadonlyArray<{ title: string }>,
     recentSummaries: ReadonlyArray<{ title: string }>,